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` 文件:
|
||||
|
||||
```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:
|
||||
本组件库使用 Tailwind CSS v4,配置方式已改变。在你的主 CSS 文件(如 `src/styles/tailwind.css`)中:
|
||||
|
||||
```css
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import 'tailwindcss';
|
||||
|
||||
/* 必须扫描组件库文件以提取类名 */
|
||||
@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:
|
||||
|
||||
|
||||
@ -13,7 +13,9 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"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": [
|
||||
"ui",
|
||||
@ -46,11 +48,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@heroui/react": "^2.8.4",
|
||||
"@storybook/addon-docs": "^9.1.6",
|
||||
"@storybook/react-vite": "^9.1.6",
|
||||
"@types/react": "^19.0.12",
|
||||
"@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",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"storybook": "^9.1.6",
|
||||
"styled-components": "^6.1.15",
|
||||
"tsdown": "^0.12.9",
|
||||
"typescript": "^5.6.2",
|
||||
"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,
|
||||
dts: true,
|
||||
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