Merge branch 'v2' of github.com:CherryHQ/cherry-studio into v2

This commit is contained in:
fullex 2025-09-16 00:40:48 +08:00
commit d8f4825e5e
17 changed files with 2051 additions and 79 deletions

15
packages/ui/.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
node_modules/
dist/
*.log
.DS_Store
# Storybook build output
storybook-static/
# IDE
.vscode/
.idea/
# Temporary files
*.tmp
*.temp

View 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

View 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

View File

@ -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

View File

@ -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"

View 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 查看组件文档。

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View File

@ -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>
)
}

View 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>
)
]
}

View File

@ -0,0 +1,3 @@
// hero.ts
import { heroui } from '@heroui/react'
export default heroui()

View 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 *));

View File

@ -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()]
}

View File

@ -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'
]
})

1113
yarn.lock

File diff suppressed because it is too large Load Diff