mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 02:20:10 +08:00
Merge branch 'v2' of github.com:CherryHQ/cherry-studio into v2
This commit is contained in:
commit
d8f4825e5e
15
packages/ui/.gitignore
vendored
Normal file
15
packages/ui/.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Storybook build output
|
||||||
|
storybook-static/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
17
packages/ui/.storybook/main.ts
Normal file
17
packages/ui/.storybook/main.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/react-vite'
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../stories/components/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||||
|
addons: ['@storybook/addon-docs'],
|
||||||
|
framework: '@storybook/react-vite',
|
||||||
|
viteFinal: async (config) => {
|
||||||
|
const { mergeConfig } = await import('vite')
|
||||||
|
// 动态导入 @tailwindcss/vite 以避免 ESM/CJS 兼容性问题
|
||||||
|
const tailwindPlugin = (await import('@tailwindcss/vite')).default
|
||||||
|
return mergeConfig(config, {
|
||||||
|
plugins: [tailwindPlugin()]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
16
packages/ui/.storybook/preview.tsx
Normal file
16
packages/ui/.storybook/preview.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import '../stories/tailwind.css'
|
||||||
|
|
||||||
|
import type { Preview } from '@storybook/react'
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default preview
|
||||||
@ -22,54 +22,34 @@ npm install @heroui/react framer-motion react react-dom tailwindcss
|
|||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
|
|
||||||
### 1. Tailwind CSS 配置
|
### 1. Tailwind CSS v4 配置
|
||||||
|
|
||||||
在你的项目根目录创建 `tailwind.config.js` 文件:
|
本组件库使用 Tailwind CSS v4,配置方式已改变。在你的主 CSS 文件(如 `src/styles/tailwind.css`)中:
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { heroui } = require('@heroui/react')
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
// 你的应用内容
|
|
||||||
'./src/**/*.{js,ts,jsx,tsx}',
|
|
||||||
'./app/**/*.{js,ts,jsx,tsx}',
|
|
||||||
'./pages/**/*.{js,ts,jsx,tsx}',
|
|
||||||
'./components/**/*.{js,ts,jsx,tsx}',
|
|
||||||
|
|
||||||
// 包含 @cherrystudio/ui 组件
|
|
||||||
'./node_modules/@cherrystudio/ui/dist/**/*.{js,ts,jsx,tsx}',
|
|
||||||
|
|
||||||
// 包含 HeroUI 主题
|
|
||||||
'./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
// 你的自定义主题扩展
|
|
||||||
}
|
|
||||||
},
|
|
||||||
darkMode: 'class',
|
|
||||||
plugins: [
|
|
||||||
heroui({
|
|
||||||
// HeroUI 主题配置
|
|
||||||
// 参考: https://heroui.com/docs/customization/theme
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. CSS 导入
|
|
||||||
|
|
||||||
在你的主 CSS 文件中导入 Tailwind:
|
|
||||||
|
|
||||||
```css
|
```css
|
||||||
@tailwind base;
|
@import 'tailwindcss';
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
/* 必须扫描组件库文件以提取类名 */
|
||||||
|
@source '../node_modules/@cherrystudio/ui/dist/**/*.{js,mjs}';
|
||||||
|
|
||||||
|
/* 你的应用源文件 */
|
||||||
|
@source './src/**/*.{js,ts,jsx,tsx}';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 如果你的应用直接使用 HeroUI 组件,需要添加:
|
||||||
|
* @source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||||
|
* @plugin '@heroui/react/plugin';
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 自定义主题配置(可选) */
|
||||||
|
@theme {
|
||||||
|
/* 你的主题扩展 */
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Provider 配置
|
注意:Tailwind CSS v4 不再使用 `tailwind.config.js` 文件,所有配置都在 CSS 中完成。
|
||||||
|
|
||||||
|
### 2. Provider 配置
|
||||||
|
|
||||||
在你的 App 根组件中添加 HeroUI Provider:
|
在你的 App 根组件中添加 HeroUI Provider:
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,9 @@
|
|||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"lint": "eslint src --ext .ts,.tsx --fix",
|
"lint": "eslint src --ext .ts,.tsx --fix",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"build-storybook": "storybook build"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ui",
|
"ui",
|
||||||
@ -46,11 +48,18 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@heroui/react": "^2.8.4",
|
"@heroui/react": "^2.8.4",
|
||||||
|
"@storybook/addon-docs": "^9.1.6",
|
||||||
|
"@storybook/react-vite": "^9.1.6",
|
||||||
"@types/react": "^19.0.12",
|
"@types/react": "^19.0.12",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@types/styled-components": "^5.1.34",
|
||||||
|
"antd": "^5.22.5",
|
||||||
|
"eslint-plugin-storybook": "9.1.6",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"storybook": "^9.1.6",
|
||||||
|
"styled-components": "^6.1.15",
|
||||||
"tsdown": "^0.12.9",
|
"tsdown": "^0.12.9",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
|
|||||||
41
packages/ui/stories/README.md
Normal file
41
packages/ui/stories/README.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Stories 文档
|
||||||
|
|
||||||
|
这里存放所有组件的 Storybook stories 文件,与源码分离以保持项目结构清晰。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
stories/
|
||||||
|
├── components/
|
||||||
|
│ ├── base/ # 基础组件 stories
|
||||||
|
│ ├── display/ # 展示组件 stories
|
||||||
|
│ ├── interactive/ # 交互组件 stories
|
||||||
|
│ ├── icons/ # 图标组件 stories
|
||||||
|
│ ├── layout/ # 布局组件 stories
|
||||||
|
│ └── composite/ # 复合组件 stories
|
||||||
|
└── README.md # 本说明文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 命名约定
|
||||||
|
|
||||||
|
- 文件名格式:`ComponentName.stories.tsx`
|
||||||
|
- Story 标题格式:`分类/组件名`,如 `Base/CustomTag`
|
||||||
|
- 导入路径:使用相对路径导入源码组件,如 `../../../src/components/base/ComponentName`
|
||||||
|
|
||||||
|
## 编写指南
|
||||||
|
|
||||||
|
每个 stories 文件应该包含:
|
||||||
|
|
||||||
|
1. **Default** - 基本用法示例
|
||||||
|
2. **Variants** - 不同变体/状态
|
||||||
|
3. **Interactive** - 交互行为演示(如果适用)
|
||||||
|
4. **Use Cases** - 实际使用场景
|
||||||
|
|
||||||
|
## 启动 Storybook
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/ui
|
||||||
|
yarn storybook
|
||||||
|
```
|
||||||
|
|
||||||
|
访问 http://localhost:6006 查看组件文档。
|
||||||
123
packages/ui/stories/components/base/CustomTag.stories.tsx
Normal file
123
packages/ui/stories/components/base/CustomTag.stories.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||||
|
import { AlertTriangleIcon, StarIcon } from 'lucide-react'
|
||||||
|
import { action } from 'storybook/actions'
|
||||||
|
|
||||||
|
import CustomTag from '../../../src/components/base/CustomTag'
|
||||||
|
|
||||||
|
const meta: Meta<typeof CustomTag> = {
|
||||||
|
title: 'Base/CustomTag',
|
||||||
|
component: CustomTag,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered'
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { control: 'color' },
|
||||||
|
size: { control: { type: 'range', min: 8, max: 24, step: 1 } },
|
||||||
|
disabled: { control: 'boolean' },
|
||||||
|
inactive: { control: 'boolean' },
|
||||||
|
closable: { control: 'boolean' },
|
||||||
|
onClose: { action: 'closed' },
|
||||||
|
onClick: { action: 'clicked' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
// 基础示例
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
children: '默认标签',
|
||||||
|
color: '#1890ff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 带图标
|
||||||
|
export const WithIcon: Story = {
|
||||||
|
args: {
|
||||||
|
children: '带图标',
|
||||||
|
color: '#52c41a',
|
||||||
|
icon: <StarIcon size={12} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可关闭
|
||||||
|
export const Closable: Story = {
|
||||||
|
args: {
|
||||||
|
children: '可关闭标签',
|
||||||
|
color: '#fa8c16',
|
||||||
|
closable: true,
|
||||||
|
onClose: action('tag-closed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不同尺寸
|
||||||
|
export const Sizes: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<CustomTag color="#1890ff" size={10}>
|
||||||
|
小号
|
||||||
|
</CustomTag>
|
||||||
|
<CustomTag color="#1890ff" size={14}>
|
||||||
|
中号
|
||||||
|
</CustomTag>
|
||||||
|
<CustomTag color="#1890ff" size={18}>
|
||||||
|
大号
|
||||||
|
</CustomTag>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不同状态
|
||||||
|
export const States: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<CustomTag color="#52c41a">正常</CustomTag>
|
||||||
|
<CustomTag color="#52c41a" disabled>
|
||||||
|
禁用
|
||||||
|
</CustomTag>
|
||||||
|
<CustomTag color="#52c41a" inactive>
|
||||||
|
未激活
|
||||||
|
</CustomTag>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<CustomTag color="#1890ff" onClick={action('clicked')}>
|
||||||
|
可点击
|
||||||
|
</CustomTag>
|
||||||
|
<CustomTag color="#fa541c" tooltip="这是一个提示">
|
||||||
|
带提示
|
||||||
|
</CustomTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实际使用场景
|
||||||
|
export const UseCases: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2">技能标签:</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<CustomTag color="#1890ff">React</CustomTag>
|
||||||
|
<CustomTag color="#52c41a">TypeScript</CustomTag>
|
||||||
|
<CustomTag color="#fa8c16">Tailwind</CustomTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2">状态标签:</h4>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<CustomTag color="#52c41a" icon={<AlertTriangleIcon size={12} />}>
|
||||||
|
进行中
|
||||||
|
</CustomTag>
|
||||||
|
<CustomTag color="#fa541c" closable onClose={action('task-removed')}>
|
||||||
|
待处理
|
||||||
|
</CustomTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
48
packages/ui/stories/components/base/ErrorTag.stories.tsx
Normal file
48
packages/ui/stories/components/base/ErrorTag.stories.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||||
|
|
||||||
|
import { ErrorTag } from '../../../src/components/base/ErrorTag'
|
||||||
|
|
||||||
|
const meta: Meta<typeof ErrorTag> = {
|
||||||
|
title: 'Base/ErrorTag',
|
||||||
|
component: ErrorTag,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered'
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
iconSize: { control: { type: 'range', min: 10, max: 20, step: 1 } },
|
||||||
|
message: { control: 'text' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
message: '错误信息'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServerError: Story = {
|
||||||
|
args: {
|
||||||
|
message: '服务器连接失败'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ValidationError: Story = {
|
||||||
|
args: {
|
||||||
|
message: '数据验证失败',
|
||||||
|
iconSize: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Examples: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<ErrorTag message="操作失败" />
|
||||||
|
<ErrorTag message="权限不足" />
|
||||||
|
<ErrorTag message="文件上传失败" iconSize={18} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
110
packages/ui/stories/components/base/SuccessTag.stories.tsx
Normal file
110
packages/ui/stories/components/base/SuccessTag.stories.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import { SuccessTag } from '../../../src/components/base/SuccessTag'
|
||||||
|
|
||||||
|
const meta: Meta<typeof SuccessTag> = {
|
||||||
|
title: 'Base/SuccessTag',
|
||||||
|
component: SuccessTag,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered'
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
iconSize: { control: { type: 'range', min: 10, max: 24, step: 1 } },
|
||||||
|
message: { control: 'text' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
// Default
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
message: 'Success'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different Messages
|
||||||
|
export const DifferentMessages: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<SuccessTag message="Operation completed" />
|
||||||
|
<SuccessTag message="File saved successfully" />
|
||||||
|
<SuccessTag message="Data uploaded" />
|
||||||
|
<SuccessTag message="Connection established" />
|
||||||
|
<SuccessTag message="Task finished" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different Icon Sizes
|
||||||
|
export const IconSizes: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<SuccessTag iconSize={10} message="Small icon" />
|
||||||
|
<SuccessTag iconSize={14} message="Default icon" />
|
||||||
|
<SuccessTag iconSize={18} message="Large icon" />
|
||||||
|
<SuccessTag iconSize={24} message="Extra large icon" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// In Context
|
||||||
|
export const InContext: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="rounded-lg border border-gray-200 p-4">
|
||||||
|
<h3 className="mb-2 font-semibold">Form Submission</h3>
|
||||||
|
<p className="mb-3 text-sm text-gray-600">Your form has been processed.</p>
|
||||||
|
<SuccessTag message="Form submitted successfully" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-gray-200 p-4">
|
||||||
|
<h3 className="mb-2 font-semibold">File Upload</h3>
|
||||||
|
<div className="mb-2 space-y-2">
|
||||||
|
<div className="text-sm">document.pdf</div>
|
||||||
|
<div className="text-sm">image.png</div>
|
||||||
|
<div className="text-sm">data.csv</div>
|
||||||
|
</div>
|
||||||
|
<SuccessTag message="3 files uploaded" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-gray-200 p-4">
|
||||||
|
<h3 className="mb-2 font-semibold">System Status</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<SuccessTag message="All systems operational" />
|
||||||
|
<SuccessTag message="Database connected" />
|
||||||
|
<SuccessTag message="API responding" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Cases
|
||||||
|
export const UseCases: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium">Actions</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<SuccessTag message="Saved" />
|
||||||
|
<SuccessTag message="Published" />
|
||||||
|
<SuccessTag message="Deployed" />
|
||||||
|
<SuccessTag message="Synced" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium">States</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<SuccessTag message="Active" />
|
||||||
|
<SuccessTag message="Online" />
|
||||||
|
<SuccessTag message="Ready" />
|
||||||
|
<SuccessTag message="Verified" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
48
packages/ui/stories/components/base/WarnTag.stories.tsx
Normal file
48
packages/ui/stories/components/base/WarnTag.stories.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||||
|
|
||||||
|
import { WarnTag } from '../../../src/components/base/WarnTag'
|
||||||
|
|
||||||
|
const meta: Meta<typeof WarnTag> = {
|
||||||
|
title: 'Base/WarnTag',
|
||||||
|
component: WarnTag,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered'
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
iconSize: { control: { type: 'range', min: 10, max: 20, step: 1 } },
|
||||||
|
message: { control: 'text' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
message: '警告信息'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LongMessage: Story = {
|
||||||
|
args: {
|
||||||
|
message: '这是一个比较长的警告信息'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomIconSize: Story = {
|
||||||
|
args: {
|
||||||
|
message: '自定义图标大小',
|
||||||
|
iconSize: 18
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Examples: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<WarnTag message="表单验证失败" />
|
||||||
|
<WarnTag message="网络连接不稳定" />
|
||||||
|
<WarnTag message="存储空间不足" iconSize={16} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,215 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import HorizontalScrollContainer from '../../../src/components/layout/HorizontalScrollContainer'
|
||||||
|
|
||||||
|
const meta: Meta<typeof HorizontalScrollContainer> = {
|
||||||
|
title: 'Layout/HorizontalScrollContainer',
|
||||||
|
component: HorizontalScrollContainer,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered'
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
scrollDistance: { control: { type: 'range', min: 50, max: 500, step: 50 } },
|
||||||
|
gap: { control: 'text' },
|
||||||
|
expandable: { control: 'boolean' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
// Default example
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
children: (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{Array.from({ length: 20 }, (_, i) => (
|
||||||
|
<div key={i} className="rounded bg-gray-100 px-4 py-2 whitespace-nowrap">
|
||||||
|
Item {i + 1}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
scrollDistance: 200
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="w-96">
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// With Tags
|
||||||
|
export const WithTags: Story = {
|
||||||
|
args: {
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
{[
|
||||||
|
'React',
|
||||||
|
'TypeScript',
|
||||||
|
'JavaScript',
|
||||||
|
'HTML',
|
||||||
|
'CSS',
|
||||||
|
'Node.js',
|
||||||
|
'Express',
|
||||||
|
'MongoDB',
|
||||||
|
'PostgreSQL',
|
||||||
|
'Docker',
|
||||||
|
'Kubernetes',
|
||||||
|
'AWS',
|
||||||
|
'Azure',
|
||||||
|
'GraphQL',
|
||||||
|
'REST API'
|
||||||
|
].map((tag) => (
|
||||||
|
<span key={tag} className="rounded-full bg-blue-500 px-3 py-1 text-xs whitespace-nowrap text-white">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
gap: '8px'
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="w-80">
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expandable
|
||||||
|
export const Expandable: Story = {
|
||||||
|
args: {
|
||||||
|
expandable: true,
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
{['Frontend', 'Backend', 'DevOps', 'Mobile', 'Desktop', 'Web', 'Cloud', 'Database', 'Security', 'Testing'].map(
|
||||||
|
(category) => (
|
||||||
|
<div key={category} className="rounded bg-green-500 px-3.5 py-1.5 text-sm whitespace-nowrap text-white">
|
||||||
|
{category}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
gap: '10px'
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="w-96">
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// With Cards
|
||||||
|
export const WithCards: Story = {
|
||||||
|
args: {
|
||||||
|
scrollDistance: 300,
|
||||||
|
gap: '16px',
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: 10 }, (_, i) => (
|
||||||
|
<div key={i} className="min-w-[200px] rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
|
||||||
|
<h4 className="mb-2 font-semibold">Card {i + 1}</h4>
|
||||||
|
<p className="text-sm text-gray-600">This is a sample card content for demonstration purposes.</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="w-[600px]">
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interactive Example
|
||||||
|
export const Interactive: Story = {
|
||||||
|
render: function InteractiveExample() {
|
||||||
|
const [items, setItems] = useState([
|
||||||
|
'Apple',
|
||||||
|
'Banana',
|
||||||
|
'Cherry',
|
||||||
|
'Date',
|
||||||
|
'Elderberry',
|
||||||
|
'Fig',
|
||||||
|
'Grape',
|
||||||
|
'Honeydew',
|
||||||
|
'Kiwi',
|
||||||
|
'Lemon',
|
||||||
|
'Mango',
|
||||||
|
'Orange'
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-96">
|
||||||
|
<HorizontalScrollContainer gap="8px" scrollDistance={150}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item}
|
||||||
|
className="cursor-pointer rounded-2xl bg-orange-500 px-4 py-2 whitespace-nowrap text-white hover:bg-orange-600"
|
||||||
|
onClick={() => alert(`Clicked: ${item}`)}>
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</HorizontalScrollContainer>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mt-4 rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
|
||||||
|
onClick={() => setItems([...items, `Item ${items.length + 1}`])}>
|
||||||
|
Add Item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different Gaps
|
||||||
|
export const DifferentGaps: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex w-96 flex-col gap-6">
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 font-semibold">Small Gap (4px)</h4>
|
||||||
|
<HorizontalScrollContainer gap="4px">
|
||||||
|
{Array.from({ length: 15 }, (_, i) => (
|
||||||
|
<span key={i} className="rounded bg-purple-600 px-3 py-1.5 text-white">
|
||||||
|
Item {i + 1}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</HorizontalScrollContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 font-semibold">Medium Gap (12px)</h4>
|
||||||
|
<HorizontalScrollContainer gap="12px">
|
||||||
|
{Array.from({ length: 15 }, (_, i) => (
|
||||||
|
<span key={i} className="rounded bg-cyan-500 px-3 py-1.5 text-white">
|
||||||
|
Item {i + 1}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</HorizontalScrollContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 font-semibold">Large Gap (20px)</h4>
|
||||||
|
<HorizontalScrollContainer gap="20px">
|
||||||
|
{Array.from({ length: 15 }, (_, i) => (
|
||||||
|
<span key={i} className="rounded bg-pink-500 px-3 py-1.5 text-white">
|
||||||
|
Item {i + 1}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</HorizontalScrollContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
259
packages/ui/stories/components/layout/Scrollbar.stories.tsx
Normal file
259
packages/ui/stories/components/layout/Scrollbar.stories.tsx
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||||
|
|
||||||
|
import Scrollbar from '../../../src/components/layout/Scrollbar'
|
||||||
|
|
||||||
|
const meta: Meta<typeof Scrollbar> = {
|
||||||
|
title: 'Layout/Scrollbar',
|
||||||
|
component: Scrollbar,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered'
|
||||||
|
},
|
||||||
|
tags: ['autodocs']
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
// Default example
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
children: (
|
||||||
|
<div className="p-4">
|
||||||
|
{Array.from({ length: 50 }, (_, i) => (
|
||||||
|
<p key={i} className="mb-2">
|
||||||
|
Line {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="w-96 h-64 border border-gray-300 rounded">
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// With Cards
|
||||||
|
export const WithCards: Story = {
|
||||||
|
args: {
|
||||||
|
children: (
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{Array.from({ length: 20 }, (_, i) => (
|
||||||
|
<div key={i} className="p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
|
||||||
|
<h3 className="mb-2 text-lg font-semibold">Card {i + 1}</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
This is a sample card with some content to demonstrate scrolling behavior.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="w-96 h-96 bg-gray-50 rounded-lg">
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal Layout
|
||||||
|
export const HorizontalContent: Story = {
|
||||||
|
args: {
|
||||||
|
children: (
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex gap-4 mb-4">
|
||||||
|
{Array.from({ length: 10 }, (_, i) => (
|
||||||
|
<div key={i} className="min-w-[150px] p-3 bg-blue-100 rounded">
|
||||||
|
Column {i + 1}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{Array.from({ length: 30 }, (_, i) => (
|
||||||
|
<p key={i} className="mb-2">
|
||||||
|
Row {i + 1}: Additional content to enable vertical scrolling
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="w-[500px] h-80 border border-gray-300 rounded overflow-x-auto">
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interactive List
|
||||||
|
export const InteractiveList: Story = {
|
||||||
|
render: () => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
console.log('Scrolling...')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-96 h-64 border border-gray-300 rounded">
|
||||||
|
<Scrollbar onScroll={handleScroll}>
|
||||||
|
<div className="p-4">
|
||||||
|
{Array.from({ length: 30 }, (_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="mb-2 p-3 bg-gray-100 rounded cursor-pointer hover:bg-gray-200 transition-colors"
|
||||||
|
onClick={() => alert(`Clicked item ${i + 1}`)}>
|
||||||
|
Interactive Item {i + 1}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Scrollbar>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code Block
|
||||||
|
export const CodeBlock: Story = {
|
||||||
|
args: {
|
||||||
|
children: (
|
||||||
|
<pre className="p-4 font-mono text-sm">
|
||||||
|
{`function calculateTotal(items) {
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.price && item.quantity) {
|
||||||
|
total += item.price * item.quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ name: 'Apple', price: 0.5, quantity: 10 },
|
||||||
|
{ name: 'Banana', price: 0.3, quantity: 15 },
|
||||||
|
{ name: 'Orange', price: 0.6, quantity: 8 },
|
||||||
|
{ name: 'Grape', price: 2.0, quantity: 3 },
|
||||||
|
{ name: 'Watermelon', price: 5.0, quantity: 1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const totalCost = calculateTotal(items);
|
||||||
|
console.log('Total cost:', totalCost);
|
||||||
|
|
||||||
|
// More code to demonstrate scrolling
|
||||||
|
class ShoppingCart {
|
||||||
|
constructor() {
|
||||||
|
this.items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(item) {
|
||||||
|
this.items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(name) {
|
||||||
|
this.items = this.items.filter(item => item.name !== name);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotal() {
|
||||||
|
return calculateTotal(this.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkout() {
|
||||||
|
const total = this.getTotal();
|
||||||
|
if (total > 0) {
|
||||||
|
console.log('Processing payment...');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="w-[600px] h-96 bg-gray-900 text-green-400 rounded-lg overflow-hidden">
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Long Article
|
||||||
|
export const LongArticle: Story = {
|
||||||
|
args: {
|
||||||
|
children: (
|
||||||
|
<article className="p-6 max-w-prose">
|
||||||
|
<h1 className="mb-4 text-2xl font-bold">The Art of Scrolling</h1>
|
||||||
|
|
||||||
|
<p className="mb-4">
|
||||||
|
Scrolling is a fundamental interaction pattern in user interfaces. It allows users to navigate through content
|
||||||
|
that exceeds the visible viewport, making it possible to present large amounts of information in a limited space.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="mb-3 text-xl font-semibold">History of Scrolling</h2>
|
||||||
|
<p className="mb-4">
|
||||||
|
The concept of scrolling dates back to the early days of computing, when terminal displays could only show a
|
||||||
|
limited number of lines. As content grew beyond what could fit on a single screen, the need for scrolling became
|
||||||
|
apparent.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="mb-3 text-xl font-semibold">Types of Scrolling</h2>
|
||||||
|
<ul className="mb-4 ml-6 list-disc">
|
||||||
|
<li className="mb-2">Vertical Scrolling - The most common type</li>
|
||||||
|
<li className="mb-2">Horizontal Scrolling - Often used for timelines and galleries</li>
|
||||||
|
<li className="mb-2">Infinite Scrolling - Continuously loads new content</li>
|
||||||
|
<li className="mb-2">Parallax Scrolling - Creates depth through different scroll speeds</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 className="mb-3 text-xl font-semibold">Best Practices</h2>
|
||||||
|
<p className="mb-4">
|
||||||
|
When implementing scrolling in your applications, consider the following best practices:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ol className="mb-4 ml-6 list-decimal">
|
||||||
|
<li className="mb-2">Always provide visual feedback for scrollable areas</li>
|
||||||
|
<li className="mb-2">Ensure scroll performance is smooth and responsive</li>
|
||||||
|
<li className="mb-2">Consider keyboard navigation for accessibility</li>
|
||||||
|
<li className="mb-2">Use appropriate scroll indicators</li>
|
||||||
|
<li className="mb-2">Test on various devices and screen sizes</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p className="mb-4">
|
||||||
|
Modern web technologies have made it easier than ever to implement sophisticated scrolling behaviors. CSS
|
||||||
|
properties like scroll-behavior and overscroll-behavior provide fine-grained control over the scrolling
|
||||||
|
experience.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="mb-3 text-xl font-semibold">Performance Considerations</h2>
|
||||||
|
<p className="mb-4">
|
||||||
|
Scrolling performance is crucial for user experience. Poor scrolling performance can make an application feel
|
||||||
|
sluggish and unresponsive. Key factors affecting scroll performance include:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="mb-4 ml-6 list-disc">
|
||||||
|
<li className="mb-2">DOM complexity and size</li>
|
||||||
|
<li className="mb-2">CSS animations and transforms</li>
|
||||||
|
<li className="mb-2">JavaScript event handlers</li>
|
||||||
|
<li className="mb-2">Image loading and rendering</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p className="mb-4">
|
||||||
|
To optimize scrolling performance, consider using techniques like virtual scrolling for large lists, debouncing
|
||||||
|
scroll event handlers, and leveraging CSS transforms for animations.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="w-[600px] h-96 bg-white border border-gray-300 rounded-lg">
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
3
packages/ui/stories/hero.ts
Normal file
3
packages/ui/stories/hero.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// hero.ts
|
||||||
|
import { heroui } from '@heroui/react'
|
||||||
|
export default heroui()
|
||||||
13
packages/ui/stories/tailwind.css
Normal file
13
packages/ui/stories/tailwind.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/* Storybook 专用的 Tailwind CSS 配置 */
|
||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
/* 扫描组件文件 */
|
||||||
|
@source '../src/components/**/*.{js,ts,jsx,tsx}';
|
||||||
|
|
||||||
|
/* 扫描 stories 文件 */
|
||||||
|
@source './components/**/*.{js,ts,jsx,tsx}';
|
||||||
|
|
||||||
|
/* HeroUI 组件样式 */
|
||||||
|
@plugin './hero.ts';
|
||||||
|
@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
@ -1,28 +0,0 @@
|
|||||||
// Tailwind config for UI component library
|
|
||||||
// This config is used for development and provides a template for consumers
|
|
||||||
|
|
||||||
let heroui
|
|
||||||
try {
|
|
||||||
// Try to load heroui if available (dev environment)
|
|
||||||
heroui = require('@heroui/react').heroui
|
|
||||||
} catch (e) {
|
|
||||||
// Fallback for environments without heroui
|
|
||||||
heroui = () => ({})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
// 扫描当前包的所有组件文件
|
|
||||||
'./src/**/*.{js,ts,jsx,tsx}',
|
|
||||||
// 扫描 HeroUI 的组件样式(如果存在)
|
|
||||||
'./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
// 基础组件库主题扩展
|
|
||||||
}
|
|
||||||
},
|
|
||||||
darkMode: 'class',
|
|
||||||
plugins: [heroui()]
|
|
||||||
}
|
|
||||||
@ -12,5 +12,15 @@ export default defineConfig({
|
|||||||
clean: true,
|
clean: true,
|
||||||
dts: true,
|
dts: true,
|
||||||
tsconfig: 'tsconfig.json',
|
tsconfig: 'tsconfig.json',
|
||||||
external: ['react', 'react-dom']
|
// 将 HeroUI、Tailwind 和其他 peer dependencies 标记为外部依赖
|
||||||
|
external: [
|
||||||
|
'react',
|
||||||
|
'react-dom',
|
||||||
|
'@heroui/react',
|
||||||
|
'@heroui/theme',
|
||||||
|
'framer-motion',
|
||||||
|
'tailwindcss',
|
||||||
|
// 保留 styled-components 作为外部依赖(迁移期间)
|
||||||
|
'styled-components'
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user