refactor: restructure UI components and remove deprecated files

- Added new components including CodeEditor, CollapsibleSearchBar, and DraggableList.
- Removed obsolete components such as Button, CustomCollapse, and StatusTag.
- Updated migration status documentation and adjusted component exports.
- Enhanced package.json dependencies for better compatibility.
This commit is contained in:
MyPrototypeWhat 2025-10-28 14:14:22 +08:00
parent 57b9ca111a
commit b9a947d2fd
253 changed files with 1213 additions and 3222 deletions

View File

@ -1,152 +1,151 @@
# UI 组件库迁移状态
# UI Component Library Migration Status
## 使用示例
## Usage Example
```typescript
// @cherrystudio/ui 导入组件
import { Spinner, DividerWithText, InfoTooltip, CustomTag } from '@cherrystudio/ui'
// Import components from @cherrystudio/ui
import { Spinner, DividerWithText, InfoTooltip } from '@cherrystudio/ui'
// 在组件中使用
// Use in components
function MyComponent() {
return (
<div>
<Spinner size={24} />
<DividerWithText text="分隔文本" />
<InfoTooltip content="提示信息" />
<CustomTag color="var(--color-primary)">标签</CustomTag>
<DividerWithText text="Divider Text" />
<InfoTooltip content="Tooltip message" />
</div>
)
}
```
## 目录结构说明
## Directory Structure
```text
@packages/ui/
├── src/
│ ├── components/ # 组件主目录
│ │ ├── base/ # 基础组件(按钮、输入框、标签等)
│ │ ├── display/ # 显示组件(卡片、列表、表格等)
│ │ ├── layout/ # 布局组件(容器、网格、间距等)
│ │ ├── icons/ # 图标组件
│ │ ├── interactive/ # 交互组件(弹窗、提示、下拉等)
│ │ └── composite/ # 复合组件(多个基础组件组合而成)
│ ├── hooks/ # 自定义 React Hooks
│ └── types/ # TypeScript 类型定义
│ ├── components/ # Main components directory
│ │ ├── base/ # Basic components (buttons, inputs, labels, etc.)
│ │ ├── display/ # Display components (cards, lists, tables, etc.)
│ │ ├── layout/ # Layout components (containers, grids, spacing, etc.)
│ │ ├── icons/ # Icon components
│ │ ├── interactive/ # Interactive components (modals, tooltips, dropdowns, etc.)
│ │ └── composite/ # Composite components (made from multiple base components)
│ ├── hooks/ # Custom React Hooks
│ └── types/ # TypeScript type definitions
```
### 组件分类指南
### Component Classification Guide
提交 PR 时,请根据组件功能将其放入正确的目录:
When submitting PRs, please place components in the correct directory based on their function:
- **base**: 最基础的 UI 元素,如按钮、输入框、开关、标签等
- **display**: 用于展示内容的组件,如卡片、列表、表格、标签页等
- **layout**: 用于页面布局的组件,如容器、网格系统、分隔符等
- **icons**: 所有图标相关的组件
- **interactive**: 需要用户交互的组件,如模态框、抽屉、提示框、下拉菜单等
- **composite**: 复合组件,由多个基础组件组合而成
- **base**: Most basic UI elements like buttons, inputs, switches, labels, etc.
- **display**: Components for displaying content like cards, lists, tables, tabs, etc.
- **layout**: Components for page layout like containers, grid systems, dividers, etc.
- **icons**: All icon-related components
- **interactive**: Components requiring user interaction like modals, drawers, tooltips, dropdowns, etc.
- **composite**: Composite components made from multiple base components
## 迁移概览
## Migration Overview
- **总组件数**: 236
- **已迁移**: 34
- **已重构**: 18
- **待迁移**: 184
- **Total Components**: 236
- **Migrated**: 34
- **Refactored**: 18
- **Pending Migration**: 184
## 组件状态表
## Component Status Table
| Category | Component Name | Migration Status | Refactoring Status | Description |
| --------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **base** | | | | 基础组件 |
| | CopyButton | ✅ | ✅ | 复制按钮 |
| | CustomTag | ✅ | ✅ | 自定义标签 |
| | DividerWithText | ✅ | ✅ | 带文本的分隔线 |
| | EmojiIcon | ✅ | ✅ | 表情图标 |
| | ErrorBoundary | ✅ | ✅ | 错误边界 (通过 props 解耦) |
| | StatusTag | ✅ | ✅ | 统一状态标签(合并了 ErrorTag、SuccessTag、WarnTag、InfoTag |
| | IndicatorLight | ✅ | ✅ | 指示灯 |
| | Spinner | ✅ | ✅ | 加载动画 |
| | TextBadge | ✅ | ✅ | 文本徽标 |
| | CustomCollapse | ✅ | ✅ | 自定义折叠面板 |
| **display** | | | | 显示组件 |
| | Ellipsis | ✅ | ✅ | 文本省略 |
| | ExpandableText | ✅ | ✅ | 可展开文本 |
| | ThinkingEffect | ✅ | ✅ | 思考效果动画 |
| | EmojiAvatar | ✅ | ✅ | 表情头像 |
| | ListItem | ✅ | ✅ | 列表项 |
| | MaxContextCount | ✅ | ✅ | 最大上下文数显示 |
| | ProviderAvatar | ✅ | ✅ | 提供者头像 |
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
| | OGCard | ❌ | ❌ | OG 卡片 |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 |
| | Preview/* | ❌ | ❌ | 预览组件 |
| **layout** | | | | 布局组件 |
| | HorizontalScrollContainer | ✅ | ❌ | 水平滚动容器 |
| | Scrollbar | ✅ | ❌ | 滚动条 |
| | Layout/* | ✅ | ✅ | 布局组件 |
| | Tab/* | ❌ | ❌ | 标签页 (Redux 依赖) |
| | TopView | ❌ | ❌ | 顶部视图 (window.api 依赖) |
| **icons** | | | | 图标组件 |
| | Icon | ✅ | ✅ | 图标工厂函数和预定义图标(合并了 CopyIcon、DeleteIcon、EditIcon、RefreshIcon、ResetIcon、ToolIcon、VisionIcon、WebSearchIcon、WrapIcon、UnWrapIcon、OcrIcon |
| | FileIcons | ✅ | ❌ | 文件图标 (FileSvgIcon、FilePngIcon) |
| | ReasoningIcon | ✅ | ❌ | 推理图标 |
| | SvgSpinners180Ring | ✅ | ❌ | 旋转加载图标 |
| | ToolsCallingIcon | ✅ | ❌ | 工具调用图标 |
| **interactive** | | | | 交互组件 |
| | InfoTooltip | ✅ | ❌ | 信息提示 |
| | HelpTooltip | ✅ | ❌ | 帮助提示 |
| | WarnTooltip | ✅ | ❌ | 警告提示 |
| | EditableNumber | ✅ | ❌ | 可编辑数字 |
| | InfoPopover | ✅ | ❌ | 信息弹出框 |
| | CollapsibleSearchBar | ✅ | ❌ | 可折叠搜索栏 |
| | ImageToolButton | ✅ | ❌ | 图片工具按钮 |
| | DraggableList | ✅ | ❌ | 可拖拽列表 |
| | CodeEditor | ✅ | ❌ | 代码编辑器 |
| | EmojiPicker | ❌ | ❌ | 表情选择器 (useTheme 依赖) |
| | Selector | ✅ | ❌ | 选择器 (i18n 依赖) |
| | ModelSelector | ❌ | ❌ | 模型选择器 (Redux 依赖) |
| | LanguageSelect | ❌ | ❌ | 语言选择 |
| | TranslateButton | ❌ | ❌ | 翻译按钮 (window.api 依赖) |
| **composite** | | | | 复合组件 |
| | - | - | - | 暂无复合组件 |
| **未分类** | | | | 需要分类的组件 |
| | Popups/* (16+ 文件) | ❌ | ❌ | 弹窗组件 (业务耦合) |
| | RichEditor/* (30+ 文件) | ❌ | ❌ | 富文本编辑器 |
| | MarkdownEditor/* | ❌ | ❌ | Markdown 编辑器 |
| | MinApp/* | ❌ | ❌ | 迷你应用 (Redux 依赖) |
| | Avatar/* | ❌ | ❌ | 头像组件 |
| | ActionTools/* | ❌ | ❌ | 操作工具 |
| | CodeBlockView/* | ❌ | ❌ | 代码块视图 (window.api 依赖) |
| | ContextMenu | ❌ | ❌ | 右键菜单 (Electron API) |
| | WindowControls | ❌ | ❌ | 窗口控制 (Electron API) |
| | ErrorBoundary | ❌ | ❌ | 错误边界 (window.api 依赖) |
| Category | Component Name | Migration Status | Refactoring Status | Description |
| ----------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **base** | | | | Base components |
| | CopyButton | ✅ | ✅ | Copy button |
| | CustomTag | ✅ | ✅ | Custom tag |
| | DividerWithText | ✅ | ✅ | Divider with text |
| | EmojiIcon | ✅ | ✅ | Emoji icon |
| | ErrorBoundary | ✅ | ✅ | Error boundary (decoupled via props) |
| | StatusTag | ✅ | ✅ | Unified status tag (merged ErrorTag, SuccessTag, WarnTag, InfoTag) |
| | IndicatorLight | ✅ | ✅ | Indicator light |
| | Spinner | ✅ | ✅ | Loading spinner |
| | TextBadge | ✅ | ✅ | Text badge |
| | CustomCollapse | ✅ | ✅ | Custom collapse panel |
| **display** | | | | Display components |
| | Ellipsis | ✅ | ✅ | Text ellipsis |
| | ExpandableText | ✅ | ✅ | Expandable text |
| | ThinkingEffect | ✅ | ✅ | Thinking effect animation |
| | EmojiAvatar | ✅ | ✅ | Emoji avatar |
| | ListItem | ✅ | ✅ | List item |
| | MaxContextCount | ✅ | ✅ | Max context count display |
| | ProviderAvatar | ✅ | ✅ | Provider avatar |
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
| | OGCard | ❌ | ❌ | OG card |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
| | Preview/* | ❌ | ❌ | Preview components |
| **layout** | | | | Layout components |
| | HorizontalScrollContainer | ✅ | ❌ | Horizontal scroll container |
| | Scrollbar | ✅ | ❌ | Scrollbar |
| | Layout/* | ✅ | ✅ | Layout components |
| | Tab/* | ❌ | ❌ | Tab (Redux dependency) |
| | TopView | ❌ | ❌ | Top view (window.api dependency) |
| **icons** | | | | Icon components |
| | Icon | ✅ | ✅ | Icon factory function and predefined icons (merged CopyIcon, DeleteIcon, EditIcon, RefreshIcon, ResetIcon, ToolIcon, VisionIcon, WebSearchIcon, WrapIcon, UnWrapIcon, OcrIcon) |
| | FileIcons | ✅ | ❌ | File icons (FileSvgIcon, FilePngIcon) |
| | ReasoningIcon | ✅ | ❌ | Reasoning icon |
| | SvgSpinners180Ring | ✅ | ❌ | Spinner loading icon |
| | ToolsCallingIcon | ✅ | ❌ | Tools calling icon |
| **interactive** | | | | Interactive components |
| | InfoTooltip | ✅ | ❌ | Info tooltip |
| | HelpTooltip | ✅ | ❌ | Help tooltip |
| | WarnTooltip | ✅ | ❌ | Warning tooltip |
| | EditableNumber | ✅ | ❌ | Editable number |
| | InfoPopover | ✅ | ❌ | Info popover |
| | CollapsibleSearchBar | ✅ | ❌ | Collapsible search bar |
| | ImageToolButton | ✅ | ❌ | Image tool button |
| | DraggableList | ✅ | ❌ | Draggable list |
| | CodeEditor | ✅ | ❌ | Code editor |
| | EmojiPicker | ❌ | ❌ | Emoji picker (useTheme dependency) |
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
| | LanguageSelect | ❌ | ❌ | Language select |
| | TranslateButton | ❌ | ❌ | Translate button (window.api dependency) |
| **composite** | | | | Composite components |
| | - | - | - | No composite components yet |
| **Uncategorized** | | | | Components needing categorization |
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
| | Avatar/* | ❌ | ❌ | Avatar components |
| | ActionTools/* | ❌ | ❌ | Action tools |
| | CodeBlockView/* | ❌ | ❌ | Code block view (window.api dependency) |
| | ContextMenu | ❌ | ❌ | Context menu (Electron API) |
| | WindowControls | ❌ | ❌ | Window controls (Electron API) |
| | ErrorBoundary | ❌ | ❌ | Error boundary (window.api dependency) |
## 迁移步骤
## Migration Steps
### 第一阶段:复制迁移(当前阶段)
### Phase 1: Copy Migration (Current Phase)
- 将组件原样复制到 @packages/ui
- 保留原有依赖antd、styled-components 等)
- 在文件顶部添加原路径注释
- Copy components as-is to @packages/ui
- Retain original dependencies (antd, styled-components, etc.)
- Add original path comment at file top
### 第二阶段:重构优化
### Phase 2: Refactor and Optimize
- 移除 antd 依赖,替换为 HeroUI
- 移除 styled-components替换为 Tailwind CSS
- 优化组件 API 和类型定义
- Remove antd dependencies, replace with HeroUI
- Remove styled-components, replace with Tailwind CSS
- Optimize component APIs and type definitions
## 注意事项
## Notes
1. **不迁移**包含以下依赖的组件(解耦后可迁移):
- window.api 调用
- ReduxuseSelector、useDispatch 等)
- 其他外部数据源
1. **Do NOT migrate** components with these dependencies (can be migrated after decoupling):
- window.api calls
- Redux (useSelector, useDispatch, etc.)
- Other external data sources
2. **可迁移**但需要后续解耦的组件:
- 使用 i18n 的组件(将 i18n 改为 props 传入)
- 使用 antd 的组件(后续替换为 HeroUI
2. **Can migrate** but need decoupling later:
- Components using i18n (change i18n to props)
- Components using antd (replace with HeroUI later)
3. **提交规范**
- 每次 PR 专注于一个类别的组件
- 确保所有迁移的组件都有导出
- 更新此文档的迁移状态
3. **Submission Guidelines**:
- Each PR should focus on one category of components
- Ensure all migrated components are exported
- Update migration status in this document

View File

@ -1,151 +0,0 @@
# UI Component Library Migration Status
## Usage Example
```typescript
// Import components from @cherrystudio/ui
import { Spinner, DividerWithText, InfoTooltip } from '@cherrystudio/ui'
// Use in components
function MyComponent() {
return (
<div>
<Spinner size={24} />
<DividerWithText text="Divider Text" />
<InfoTooltip content="Tooltip message" />
</div>
)
}
```
## Directory Structure
```text
@packages/ui/
├── src/
│ ├── components/ # Main components directory
│ │ ├── base/ # Basic components (buttons, inputs, labels, etc.)
│ │ ├── display/ # Display components (cards, lists, tables, etc.)
│ │ ├── layout/ # Layout components (containers, grids, spacing, etc.)
│ │ ├── icons/ # Icon components
│ │ ├── interactive/ # Interactive components (modals, tooltips, dropdowns, etc.)
│ │ └── composite/ # Composite components (made from multiple base components)
│ ├── hooks/ # Custom React Hooks
│ └── types/ # TypeScript type definitions
```
### Component Classification Guide
When submitting PRs, please place components in the correct directory based on their function:
- **base**: Most basic UI elements like buttons, inputs, switches, labels, etc.
- **display**: Components for displaying content like cards, lists, tables, tabs, etc.
- **layout**: Components for page layout like containers, grid systems, dividers, etc.
- **icons**: All icon-related components
- **interactive**: Components requiring user interaction like modals, drawers, tooltips, dropdowns, etc.
- **composite**: Composite components made from multiple base components
## Migration Overview
- **Total Components**: 236
- **Migrated**: 34
- **Refactored**: 18
- **Pending Migration**: 184
## Component Status Table
| Category | Component Name | Migration Status | Refactoring Status | Description |
| ----------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **base** | | | | Base components |
| | CopyButton | ✅ | ✅ | Copy button |
| | CustomTag | ✅ | ✅ | Custom tag |
| | DividerWithText | ✅ | ✅ | Divider with text |
| | EmojiIcon | ✅ | ✅ | Emoji icon |
| | ErrorBoundary | ✅ | ✅ | Error boundary (decoupled via props) |
| | StatusTag | ✅ | ✅ | Unified status tag (merged ErrorTag, SuccessTag, WarnTag, InfoTag) |
| | IndicatorLight | ✅ | ✅ | Indicator light |
| | Spinner | ✅ | ✅ | Loading spinner |
| | TextBadge | ✅ | ✅ | Text badge |
| | CustomCollapse | ✅ | ✅ | Custom collapse panel |
| **display** | | | | Display components |
| | Ellipsis | ✅ | ✅ | Text ellipsis |
| | ExpandableText | ✅ | ✅ | Expandable text |
| | ThinkingEffect | ✅ | ✅ | Thinking effect animation |
| | EmojiAvatar | ✅ | ✅ | Emoji avatar |
| | ListItem | ✅ | ✅ | List item |
| | MaxContextCount | ✅ | ✅ | Max context count display |
| | ProviderAvatar | ✅ | ✅ | Provider avatar |
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
| | OGCard | ❌ | ❌ | OG card |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
| | Preview/* | ❌ | ❌ | Preview components |
| **layout** | | | | Layout components |
| | HorizontalScrollContainer | ✅ | ❌ | Horizontal scroll container |
| | Scrollbar | ✅ | ❌ | Scrollbar |
| | Layout/* | ✅ | ✅ | Layout components |
| | Tab/* | ❌ | ❌ | Tab (Redux dependency) |
| | TopView | ❌ | ❌ | Top view (window.api dependency) |
| **icons** | | | | Icon components |
| | Icon | ✅ | ✅ | Icon factory function and predefined icons (merged CopyIcon, DeleteIcon, EditIcon, RefreshIcon, ResetIcon, ToolIcon, VisionIcon, WebSearchIcon, WrapIcon, UnWrapIcon, OcrIcon) |
| | FileIcons | ✅ | ❌ | File icons (FileSvgIcon, FilePngIcon) |
| | ReasoningIcon | ✅ | ❌ | Reasoning icon |
| | SvgSpinners180Ring | ✅ | ❌ | Spinner loading icon |
| | ToolsCallingIcon | ✅ | ❌ | Tools calling icon |
| **interactive** | | | | Interactive components |
| | InfoTooltip | ✅ | ❌ | Info tooltip |
| | HelpTooltip | ✅ | ❌ | Help tooltip |
| | WarnTooltip | ✅ | ❌ | Warning tooltip |
| | EditableNumber | ✅ | ❌ | Editable number |
| | InfoPopover | ✅ | ❌ | Info popover |
| | CollapsibleSearchBar | ✅ | ❌ | Collapsible search bar |
| | ImageToolButton | ✅ | ❌ | Image tool button |
| | DraggableList | ✅ | ❌ | Draggable list |
| | CodeEditor | ✅ | ❌ | Code editor |
| | EmojiPicker | ❌ | ❌ | Emoji picker (useTheme dependency) |
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
| | LanguageSelect | ❌ | ❌ | Language select |
| | TranslateButton | ❌ | ❌ | Translate button (window.api dependency) |
| **composite** | | | | Composite components |
| | - | - | - | No composite components yet |
| **Uncategorized** | | | | Components needing categorization |
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
| | Avatar/* | ❌ | ❌ | Avatar components |
| | ActionTools/* | ❌ | ❌ | Action tools |
| | CodeBlockView/* | ❌ | ❌ | Code block view (window.api dependency) |
| | ContextMenu | ❌ | ❌ | Context menu (Electron API) |
| | WindowControls | ❌ | ❌ | Window controls (Electron API) |
| | ErrorBoundary | ❌ | ❌ | Error boundary (window.api dependency) |
## Migration Steps
### Phase 1: Copy Migration (Current Phase)
- Copy components as-is to @packages/ui
- Retain original dependencies (antd, styled-components, etc.)
- Add original path comment at file top
### Phase 2: Refactor and Optimize
- Remove antd dependencies, replace with HeroUI
- Remove styled-components, replace with Tailwind CSS
- Optimize component APIs and type definitions
## Notes
1. **Do NOT migrate** components with these dependencies (can be migrated after decoupling):
- window.api calls
- Redux (useSelector, useDispatch, etc.)
- Other external data sources
2. **Can migrate** but need decoupling later:
- Components using i18n (change i18n to props)
- Components using antd (replace with HeroUI later)
3. **Submission Guidelines**:
- Each PR should focus on one category of components
- Ensure all migrated components are exported
- Update migration status in this document

View File

@ -4,7 +4,7 @@
"components": "@cherrystudio/ui/components",
"hooks": "@cherrystudio/ui/hooks",
"lib": "@cherrystudio/ui/lib",
"ui": "@cherrystudio/ui/components/ui",
"ui": "@cherrystudio/ui/components/primitives",
"utils": "@cherrystudio/ui/utils"
},
"iconLibrary": "lucide",

View File

@ -1,12 +0,0 @@
import type { ButtonProps as HeroUIButtonProps } from '@heroui/react'
import { Button as HeroUIButton } from '@heroui/react'
export interface ButtonProps extends HeroUIButtonProps {}
const Button = ({ ...props }: ButtonProps) => {
return <HeroUIButton {...props} />
}
Button.displayName = 'Button'
export default Button

View File

@ -1,46 +0,0 @@
import { Accordion, AccordionItem, type AccordionItemProps, type AccordionProps } from '@heroui/react'
import type { FC } from 'react'
import { memo } from 'react'
// 重新导出 HeroUI 的组件,方便直接使用
export { Accordion, AccordionItem } from '@heroui/react'
interface CustomCollapseProps {
children: React.ReactNode
accordionProps?: Omit<AccordionProps, 'children'>
accordionItemProps?: Omit<AccordionItemProps, 'children'>
}
const CustomCollapse: FC<CustomCollapseProps> = ({ children, accordionProps = {}, accordionItemProps = {} }) => {
// 解构 Accordion 的 props
const {
defaultExpandedKeys = ['1'],
variant = 'bordered',
className = '',
isDisabled = false,
...restAccordionProps
} = accordionProps
// 解构 AccordionItem 的 props
const { title = 'Collapse Panel', ...restAccordionItemProps } = accordionItemProps
return (
<Accordion
defaultExpandedKeys={defaultExpandedKeys}
variant={variant}
className={className}
isDisabled={isDisabled}
selectionMode="multiple"
{...restAccordionProps}>
<AccordionItem
key="1"
aria-label={typeof title === 'string' ? title : 'collapse-item'}
title={title}
{...restAccordionItemProps}>
{children}
</AccordionItem>
</Accordion>
)
}
export default memo(CustomCollapse)

View File

@ -1,333 +0,0 @@
# Selector 组件
基于 HeroUI Select 封装的下拉选择组件,简化了 Set 和 Selection 的转换逻辑。
## 核心特性
- ✅ **类型安全**: 单选和多选自动推断回调类型
- ✅ **智能转换**: 自动处理 `Set<Key>` 和原始值的转换
- ✅ **HeroUI 风格**: 保持与 HeroUI 生态一致的 API
- ✅ **支持数字和字符串**: 泛型支持,自动识别值类型
## 基础用法
### 单选模式(默认)
```tsx
import { Selector } from '@cherrystudio/ui'
import { useState } from 'react'
function Example() {
const [language, setLanguage] = useState('zh-CN')
const languageOptions = [
{ label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
{ label: '日本語', value: 'ja-JP' }
]
return (
<Selector
selectedKeys={language}
onSelectionChange={(value) => {
// value 类型自动推断为 string
setLanguage(value)
}}
items={languageOptions}
placeholder="选择语言"
/>
)
}
```
### 多选模式
```tsx
import { Selector } from '@cherrystudio/ui'
import { useState } from 'react'
function Example() {
const [languages, setLanguages] = useState(['zh-CN', 'en-US'])
const languageOptions = [
{ label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
{ label: '日本語', value: 'ja-JP' },
{ label: 'Français', value: 'fr-FR' }
]
return (
<Selector
selectionMode="multiple"
selectedKeys={languages}
onSelectionChange={(values) => {
// values 类型自动推断为 string[]
setLanguages(values)
}}
items={languageOptions}
placeholder="选择语言"
/>
)
}
```
### 数字类型值
```tsx
import { Selector } from '@cherrystudio/ui'
function Example() {
const [priority, setPriority] = useState<number>(1)
const priorityOptions = [
{ label: '低', value: 1 },
{ label: '中', value: 2 },
{ label: '高', value: 3 }
]
return (
<Selector<number>
selectedKeys={priority}
onSelectionChange={(value) => {
// value 类型为 number
setPriority(value)
}}
items={priorityOptions}
/>
)
}
```
### 禁用选项
```tsx
const options = [
{ label: '选项 1', value: '1' },
{ label: '选项 2 (禁用)', value: '2', disabled: true },
{ label: '选项 3', value: '3' }
]
<Selector
selectedKeys="1"
onSelectionChange={handleChange}
items={options}
/>
```
### 自定义 Label
```tsx
import { Flex } from '@cherrystudio/ui'
const options = [
{
label: (
<Flex className="items-center gap-2">
<span>🇨🇳</span>
<span>中文</span>
</Flex>
),
value: 'zh-CN'
},
{
label: (
<Flex className="items-center gap-2">
<span>🇺🇸</span>
<span>English</span>
</Flex>
),
value: 'en-US'
}
]
<Selector
selectedKeys="zh-CN"
onSelectionChange={handleChange}
items={options}
/>
```
## API
### SelectorProps
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `items` | `SelectorItem<V>[]` | - | 必填,选项列表 |
| `selectedKeys` | `V` \| `V[]` | - | 受控的选中值(单选为单个值,多选为数组) |
| `onSelectionChange` | `(key: V) => void` \| `(keys: V[]) => void` | - | 选择变化回调(类型根据 selectionMode 自动推断) |
| `selectionMode` | `'single'` \| `'multiple'` | `'single'` | 选择模式 |
| `placeholder` | `string` | - | 占位文本 |
| `disabled` | `boolean` | `false` | 是否禁用 |
| `isRequired` | `boolean` | `false` | 是否必填 |
| `label` | `ReactNode` | - | 标签文本 |
| `description` | `ReactNode` | - | 描述文本 |
| `errorMessage` | `ReactNode` | - | 错误提示 |
| ...rest | `SelectProps` | - | 其他 HeroUI Select 属性 |
### SelectorItem
```tsx
interface SelectorItem<V = string | number> {
label: string | ReactNode // 显示文本或自定义内容
value: V // 选项值
disabled?: boolean // 是否禁用
[key: string]: any // 其他自定义属性
}
```
## 类型安全
组件使用 TypeScript 条件类型,根据 `selectionMode` 自动推断回调类型:
```tsx
// 单选模式
<Selector
selectionMode="single" // 或省略(默认单选)
selectedKeys={value} // 类型: V
onSelectionChange={(v) => ...} // v 类型: V
/>
// 多选模式
<Selector
selectionMode="multiple"
selectedKeys={values} // 类型: V[]
onSelectionChange={(vs) => ...} // vs 类型: V[]
/>
```
## 与 HeroUI Select 的区别
| 特性 | HeroUI Select | Selector (本组件) |
|------|---------------|------------------|
| `selectedKeys` | `Set<Key> \| 'all'` | `V` \| `V[]` (自动转换) |
| `onSelectionChange` | `(keys: Selection) => void` | `(key: V) => void` \| `(keys: V[]) => void` |
| 单选回调 | 返回 `Set` (需手动提取) | 直接返回单个值 |
| 多选回调 | 返回 `Set` (需转数组) | 直接返回数组 |
| 类型推断 | 无 | 根据 selectionMode 自动推断 |
## 最佳实践
### 1. 显式声明 selectionMode
虽然单选是默认模式,但建议显式声明以提高代码可读性:
```tsx
// ✅ 推荐
<Selector selectionMode="single" ... />
// ⚠️ 可以但不够清晰
<Selector ... />
```
### 2. 使用泛型指定值类型
当值类型为数字或联合类型时,使用泛型获得更好的类型提示:
```tsx
// ✅ 推荐
<Selector<number> selectedKeys={priority} ... />
// ✅ 推荐(联合类型)
type Status = 'pending' | 'approved' | 'rejected'
<Selector<Status> selectedKeys={status} ... />
```
### 3. 避免在渲染时创建 items
```tsx
// ❌ 不推荐(每次渲染都创建新数组)
<Selector items={[{ label: 'A', value: '1' }]} />
// ✅ 推荐(在组件外或使用 useMemo
const items = [{ label: 'A', value: '1' }]
<Selector items={items} />
```
## 迁移指南
### 从 antd Select 迁移
```tsx
// antd Select
import { Select } from 'antd'
<Select
value={value}
onChange={(value) => onChange(value)}
options={[
{ label: 'A', value: '1' },
{ label: 'B', value: '2' }
]}
/>
// 迁移到 Selector
import { Selector } from '@cherrystudio/ui'
<Selector
selectedKeys={value} // value → selectedKeys
onSelectionChange={(value) => onChange(value)} // onChange → onSelectionChange
items={[ // options → items
{ label: 'A', value: '1' },
{ label: 'B', value: '2' }
]}
/>
```
### 从旧版 Selector 迁移
```tsx
// 旧版 Selector (返回数组)
<Selector
onSelectionChange={(values) => {
const value = values[0] // 需要手动提取
onChange(value)
}}
/>
// 新版 Selector (直接返回值)
<Selector
selectionMode="single"
onSelectionChange={(value) => {
onChange(value) // 直接使用
}}
/>
```
## 常见问题
### Q: 为什么单选模式下还需要 selectedKeys 而不是 selectedKey
A: 为了保持与 HeroUI API 命名的一致性,同时简化组件实现。组件内部会自动处理单个值和 Set 的转换。
### Q: 如何清空选择?
```tsx
// 单选模式
<Selector
selectedKeys={value}
onSelectionChange={setValue}
isClearable // 添加清空按钮
/>
// 或手动设置为 undefined
setValue(undefined)
```
### Q: 支持异步加载选项吗?
支持,配合 `isLoading` 属性使用:
```tsx
const [items, setItems] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchItems().then(data => {
setItems(data)
setLoading(false)
})
}, [])
<Selector items={items} isLoading={loading} />
```

View File

@ -1,53 +0,0 @@
import type { LucideIcon } from 'lucide-react'
import { AlertTriangleIcon, CheckIcon, CircleXIcon, InfoIcon } from 'lucide-react'
import React from 'react'
import CustomTag from '../CustomTag'
export type StatusType = 'success' | 'error' | 'warning' | 'info'
export interface StatusTagProps {
type: StatusType
message: string
iconSize?: number
icon?: React.ReactNode
color?: string
className?: string
}
const statusConfig: Record<StatusType, { Icon: LucideIcon; color: string }> = {
success: { Icon: CheckIcon, color: '#10B981' }, // green-500
error: { Icon: CircleXIcon, color: '#EF4444' }, // red-500
warning: { Icon: AlertTriangleIcon, color: '#F59E0B' }, // amber-500
info: { Icon: InfoIcon, color: '#3B82F6' } // blue-500
}
export const StatusTag: React.FC<StatusTagProps> = ({ type, message, iconSize = 14, icon, color, className }) => {
const config = statusConfig[type]
const Icon = config.Icon
const finalColor = color || config.color
const finalIcon = icon || <Icon size={iconSize} color={finalColor} />
return (
<CustomTag icon={finalIcon} color={finalColor} className={className}>
{message}
</CustomTag>
)
}
// 保留原有的导出以保持向后兼容
export const SuccessTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
<StatusTag type="success" iconSize={iconSize} message={message} />
)
export const ErrorTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
<StatusTag type="error" iconSize={iconSize} message={message} />
)
export const WarnTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
<StatusTag type="warning" iconSize={iconSize} message={message} />
)
export const InfoTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
<StatusTag type="info" iconSize={iconSize} message={message} />
)

View File

@ -1,20 +0,0 @@
// Original: src/renderer/src/components/TextBadge.tsx
import type { FC } from 'react'
interface TextBadgeProps {
text: string
style?: React.CSSProperties
className?: string
}
const TextBadge: FC<TextBadgeProps> = ({ text, style, className = '' }) => {
return (
<span
className={`text-xs text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/30 px-1.5 py-0.5 rounded font-medium ${className}`}
style={style}>
{text}
</span>
)
}
export default TextBadge

View File

@ -5,7 +5,7 @@ import { Search } from 'lucide-react'
import { motion } from 'motion/react'
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
import { Tooltip } from '../../base/Tooltip'
import { Tooltip } from '../../primitives/tooltip'
interface CollapsibleSearchBarProps {
onSearch: (text: string) => void

View File

@ -9,7 +9,7 @@ import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
import { type ScrollToOptions, useVirtualizer, type VirtualItem } from '@tanstack/react-virtual'
import { type Key, memo, useCallback, useImperativeHandle, useRef } from 'react'
import Scrollbar from '../../layout/Scrollbar'
import Scrollbar from '../Scrollbar'
import { droppableReorder } from './sort'
export interface DraggableVirtualListRef {

View File

@ -1,7 +1,7 @@
// Original path: src/renderer/src/components/TooltipIcons/HelpTooltip.tsx
import { HelpCircle } from 'lucide-react'
import { Tooltip } from '../../base/Tooltip'
import { Tooltip } from '../../primitives/tooltip'
import type { IconTooltipProps } from './types'
export const HelpTooltip = ({ iconProps, ...rest }: IconTooltipProps) => {

View File

@ -1,7 +1,7 @@
// Original: src/renderer/src/components/TooltipIcons/InfoTooltip.tsx
import { Info } from 'lucide-react'
import { Tooltip } from '../../base/Tooltip'
import { Tooltip } from '../../primitives/tooltip'
import type { IconTooltipProps } from './types'
export const InfoTooltip = ({ iconProps, ...rest }: IconTooltipProps) => {

View File

@ -1,7 +1,7 @@
// Original path: src/renderer/src/components/TooltipIcons/WarnTooltip.tsx
import { AlertTriangle } from 'lucide-react'
import { Tooltip } from '../../base/Tooltip'
import { Tooltip } from '../../primitives/tooltip'
import type { IconTooltipProps } from './types'
export const WarnTooltip = ({ iconProps, ...rest }: IconTooltipProps) => {

View File

@ -1,6 +1,6 @@
import type { LucideProps } from 'lucide-react'
import type { TooltipProps } from '../../base/Tooltip'
import type { TooltipProps } from '../../primitives/tooltip'
export interface IconTooltipProps extends TooltipProps {
iconProps?: LucideProps

View File

@ -1,8 +1,8 @@
// Original path: src/renderer/src/components/Preview/ImageToolButton.tsx
import { memo } from 'react'
import Button from '../../base/Button'
import { Tooltip } from '../../base/Tooltip'
import { Button } from '../../primitives/button'
import { Tooltip } from '../../primitives/tooltip'
interface ImageToolButtonProps {
tooltip: string
@ -13,7 +13,7 @@ interface ImageToolButtonProps {
const ImageToolButton = ({ tooltip, icon, onPress }: ImageToolButtonProps) => {
return (
<Tooltip content={tooltip} delay={500} closeDelay={0}>
<Button radius="full" isIconOnly onPress={onPress} aria-label={tooltip}>
<Button size="icon" className="rounded-full" onClick={onPress} aria-label={tooltip}>
{icon}
</Button>
</Tooltip>

View File

@ -1,33 +1,26 @@
// Base Components
export { Avatar, AvatarGroup, type AvatarProps, EmojiAvatar } from './base/Avatar'
export { default as Button, type ButtonProps } from './base/Button'
export { default as CopyButton } from './base/CopyButton'
export { default as CustomCollapse } from './base/CustomCollapse'
export { default as CustomTag } from './base/CustomTag'
export { default as DividerWithText } from './base/DividerWithText'
export { default as EmojiIcon } from './base/EmojiIcon'
export type { CustomFallbackProps, ErrorBoundaryCustomizedProps } from './base/ErrorBoundary'
export { ErrorBoundary } from './base/ErrorBoundary'
export { default as IndicatorLight } from './base/IndicatorLight'
export { default as Spinner } from './base/Spinner'
export type { StatusTagProps, StatusType } from './base/StatusTag'
export { ErrorTag, InfoTag, StatusTag, SuccessTag, WarnTag } from './base/StatusTag'
export { DescriptionSwitch, Switch } from './base/Switch'
export { default as TextBadge } from './base/TextBadge'
export { getToastUtilities, type ToastUtilities } from './base/Toast'
export { Tooltip, type TooltipProps } from './base/Tooltip'
// Primitive Components
export { Avatar, AvatarGroup, type AvatarProps, EmojiAvatar } from './primitives/Avatar'
export { default as CopyButton } from './primitives/copyButton'
export { default as CustomTag } from './primitives/customTag'
export { default as DividerWithText } from './primitives/dividerWithText'
export { default as EmojiIcon } from './primitives/emojiIcon'
export type { CustomFallbackProps, ErrorBoundaryCustomizedProps } from './primitives/ErrorBoundary'
export { ErrorBoundary } from './primitives/ErrorBoundary'
export { default as IndicatorLight } from './primitives/indicatorLight'
export { default as Spinner } from './primitives/spinner'
export { DescriptionSwitch, Switch } from './primitives/switch'
export { getToastUtilities, type ToastUtilities } from './primitives/toast'
export { Tooltip, type TooltipProps } from './primitives/tooltip'
// Display Components
export { default as Ellipsis } from './display/Ellipsis'
export { default as ExpandableText } from './display/ExpandableText'
export { default as ListItem } from './display/ListItem'
export { default as MaxContextCount } from './display/MaxContextCount'
export { default as ThinkingEffect } from './display/ThinkingEffect'
// Layout Components
export { Box, Center, ColFlex, Flex, RowFlex, SpaceBetweenRowFlex } from './layout/Flex'
export { default as HorizontalScrollContainer } from './layout/HorizontalScrollContainer'
export { default as Scrollbar } from './layout/Scrollbar'
// Composite Components
export { default as Ellipsis } from './composites/Ellipsis'
export { default as ExpandableText } from './composites/ExpandableText'
export { Box, Center, ColFlex, Flex, RowFlex, SpaceBetweenRowFlex } from './composites/Flex'
export { default as HorizontalScrollContainer } from './composites/HorizontalScrollContainer'
export { default as ListItem } from './composites/ListItem'
export { default as MaxContextCount } from './composites/MaxContextCount'
export { default as Scrollbar } from './composites/Scrollbar'
export { default as ThinkingEffect } from './composites/ThinkingEffect'
// Icon Components
export { FilePngIcon, FileSvgIcon } from './icons/FileIcons'
@ -49,11 +42,9 @@ export {
export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring'
export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon'
/* Interactive Components */
// Selector / SearchableSelector
export { default as Selector } from './base/Selector'
export { default as SearchableSelector } from './base/Selector/SearchableSelector'
/* Selector Components */
export { default as Selector } from './primitives/Selector'
export { default as SearchableSelector } from './primitives/Selector/SearchableSelector'
export type {
MultipleSearchableSelectorProps,
MultipleSelectorProps,
@ -63,7 +54,9 @@ export type {
SelectorProps,
SingleSearchableSelectorProps,
SingleSelectorProps
} from './base/Selector/types'
} from './primitives/Selector/types'
/* Additional Composite Components */
// CodeEditor
export {
default as CodeEditor,
@ -72,28 +65,25 @@ export {
type CodeMirrorTheme,
getCmThemeByName,
getCmThemeNames
} from './interactive/CodeEditor'
} from './composites/CodeEditor'
// CollapsibleSearchBar
export { default as CollapsibleSearchBar } from './interactive/CollapsibleSearchBar'
export { default as CollapsibleSearchBar } from './composites/CollapsibleSearchBar'
// DraggableList
export { DraggableList, useDraggableReorder } from './interactive/DraggableList'
export { DraggableList, useDraggableReorder } from './composites/DraggableList'
// EditableNumber
export type { EditableNumberProps } from './interactive/EditableNumber'
// EditableNumber
export { default as EditableNumber } from './interactive/EditableNumber'
export type { EditableNumberProps } from './composites/EditableNumber'
export { default as EditableNumber } from './composites/EditableNumber'
// Tooltip variants
export { HelpTooltip, type IconTooltipProps, InfoTooltip, WarnTooltip } from './interactive/IconTooltips'
export { HelpTooltip, type IconTooltipProps, InfoTooltip, WarnTooltip } from './composites/IconTooltips'
// ImageToolButton
export { default as ImageToolButton } from './interactive/ImageToolButton'
export { default as ImageToolButton } from './composites/ImageToolButton'
// Sortable
export { Sortable } from './interactive/Sortable'
export { Sortable } from './composites/Sortable'
// Composite Components (复合组件)
// 暂无复合组件
// UI Components (shadcn)
export * from './ui/button'
export * from './ui/command'
export * from './ui/dialog'
export * from './ui/popover'
export * from './ui/shadcn-io/dropzone'
/* Shadcn Primitive Components */
export * from './primitives/button'
export * from './primitives/command'
export * from './primitives/dialog'
export * from './primitives/popover'
export * from './primitives/radioGroup'
export * from './primitives/shadcn-io/dropzone'

View File

@ -1,6 +1,7 @@
import { cn } from '@heroui/react'
import React, { memo } from 'react'
import { cn } from '../../../utils'
interface EmojiAvatarProps {
children: string
size?: number

View File

@ -1,6 +1,7 @@
import type { AvatarProps as HeroUIAvatarProps } from '@heroui/react'
import { Avatar as HeroUIAvatar, AvatarGroup as HeroUIAvatarGroup, cn } from '@heroui/react'
import { Avatar as HeroUIAvatar, AvatarGroup as HeroUIAvatarGroup } from '@heroui/react'
import { cn } from '../../../utils'
import EmojiAvatar from './EmojiAvatar'
export interface AvatarProps extends Omit<HeroUIAvatarProps, 'size'> {

View File

@ -1,10 +1,10 @@
// Original path: src/renderer/src/components/ErrorBoundary.tsx
import { Button } from '@heroui/react'
import { AlertTriangle } from 'lucide-react'
import type { ComponentType, ReactNode } from 'react'
import type { FallbackProps } from 'react-error-boundary'
import { ErrorBoundary } from 'react-error-boundary'
import { Button } from '../button'
import { formatErrorMessage } from './utils'
interface CustomFallbackProps extends FallbackProps {
@ -35,12 +35,12 @@ const DefaultFallback: ComponentType<CustomFallbackProps> = (props: CustomFallba
<p className="text-red-700 dark:text-red-300 text-sm mb-3">{formatErrorMessage(error)}</p>
<div className="flex gap-2">
{onDebugClick && (
<Button size="sm" variant="flat" color="danger" onPress={onDebugClick}>
<Button size="sm" variant="destructive" onClick={onDebugClick}>
{debugButtonText}
</Button>
)}
{onReloadClick && (
<Button size="sm" variant="flat" color="danger" onPress={onReloadClick}>
<Button size="sm" variant="destructive" onClick={onReloadClick}>
{reloadButtonText}
</Button>
)}

View File

@ -4,7 +4,7 @@ import {
DialogDescription,
DialogHeader,
DialogTitle
} from '@cherrystudio/ui/components/ui/dialog'
} from '@cherrystudio/ui/components/primitives/dialog'
import { cn } from '@cherrystudio/ui/utils'
import { Command as CommandPrimitive } from 'cmdk'
import { SearchIcon } from 'lucide-react'

View File

@ -1,8 +1,9 @@
// Original path: src/renderer/src/components/CopyButton.tsx
import { Tooltip } from '@heroui/react'
import { Copy } from 'lucide-react'
import type { FC } from 'react'
import { Tooltip } from './tooltip'
interface CopyButtonProps {
tooltip?: string
label?: string

View File

@ -1,9 +1,10 @@
// Original path: src/renderer/src/components/Tags/CustomTag.tsx
import { Tooltip } from '@heroui/react'
import { X } from 'lucide-react'
import type { CSSProperties, FC, MouseEventHandler } from 'react'
import { memo, useMemo } from 'react'
import { Tooltip } from './tooltip'
export interface CustomTagProps {
icon?: React.ReactNode
children?: React.ReactNode | string

View File

@ -1,6 +1,6 @@
'use client'
import { Button } from '@cherrystudio/ui/components/ui/button'
import { Button } from '@cherrystudio/ui/components/primitives/button'
import { cn } from '@cherrystudio/ui/utils/index'
import { UploadIcon } from 'lucide-react'
import type { ReactNode } from 'react'

View File

@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'
import { Button } from '../../../src/components'
const meta: Meta<typeof Button> = {
title: 'Components/Base/Button',
title: 'Components/Primitives/Button',
component: Button,
parameters: {
layout: 'centered'
@ -12,30 +12,16 @@ const meta: Meta<typeof Button> = {
argTypes: {
variant: {
control: { type: 'select' },
options: ['solid', 'bordered', 'light', 'flat', 'faded', 'shadow', 'ghost']
},
color: {
control: { type: 'select' },
options: ['default', 'primary', 'secondary', 'success', 'warning', 'danger']
options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']
},
size: {
control: { type: 'select' },
options: ['sm', 'md', 'lg']
options: ['default', 'sm', 'lg', 'icon', 'icon-sm', 'icon-lg']
},
radius: {
control: { type: 'select' },
options: ['none', 'sm', 'md', 'lg', 'full']
},
isDisabled: {
disabled: {
control: { type: 'boolean' }
},
isLoading: {
control: { type: 'boolean' }
},
fullWidth: {
control: { type: 'boolean' }
},
isIconOnly: {
asChild: {
control: { type: 'boolean' }
}
}
@ -55,27 +41,12 @@ export const Default: Story = {
export const Variants: Story = {
render: () => (
<div className="flex gap-2 flex-wrap">
<Button variant="solid">Solid</Button>
<Button variant="bordered">Bordered</Button>
<Button variant="light">Light</Button>
<Button variant="flat">Flat</Button>
<Button variant="faded">Faded</Button>
<Button variant="shadow">Shadow</Button>
<Button variant="default">Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
)
}
// 不同颜色
export const Colors: Story = {
render: () => (
<div className="flex gap-2 flex-wrap">
<Button color="default">Default</Button>
<Button color="primary">Primary</Button>
<Button color="secondary">Secondary</Button>
<Button color="success">Success</Button>
<Button color="warning">Warning</Button>
<Button color="danger">Danger</Button>
<Button variant="link">Link</Button>
</div>
)
}
@ -83,23 +54,21 @@ export const Colors: Story = {
// 不同尺寸
export const Sizes: Story = {
render: () => (
<div className="flex gap-2 items-center">
<div className="flex gap-2 items-center flex-wrap">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
</div>
)
}
// 不同圆角
export const Radius: Story = {
// 图标按钮
export const IconButtons: Story = {
render: () => (
<div className="flex gap-2 flex-wrap">
<Button radius="none">None</Button>
<Button radius="sm">Small</Button>
<Button radius="md">Medium</Button>
<Button radius="lg">Large</Button>
<Button radius="full">Full</Button>
<div className="flex gap-2 items-center flex-wrap">
<Button size="icon-sm">🔍</Button>
<Button size="icon">🔍</Button>
<Button size="icon-lg">🔍</Button>
</div>
)
}
@ -109,8 +78,7 @@ export const States: Story = {
render: () => (
<div className="flex gap-2 flex-wrap">
<Button>Normal</Button>
<Button isDisabled>Disabled</Button>
<Button isLoading>Loading</Button>
<Button disabled>Disabled</Button>
</div>
)
}
@ -119,9 +87,15 @@ export const States: Story = {
export const WithIcons: Story = {
render: () => (
<div className="flex gap-2 flex-wrap">
<Button startContent={<span>📧</span>}>Email</Button>
<Button endContent={<span></span>}>Next</Button>
<Button isIconOnly>🔍</Button>
<Button>
<span className="mr-2">📧</span>
Email
</Button>
<Button>
Next
<span className="ml-2"></span>
</Button>
<Button size="icon">🔍</Button>
</div>
)
}
@ -130,9 +104,7 @@ export const WithIcons: Story = {
export const FullWidth: Story = {
render: () => (
<div className="w-96">
<Button fullWidth color="primary">
Full Width Button
</Button>
<Button className="w-full">Full Width Button</Button>
</div>
)
}
@ -141,10 +113,45 @@ export const FullWidth: Story = {
export const Interactive: Story = {
render: () => (
<div className="flex gap-2 flex-wrap">
<Button onPress={() => alert('Button pressed!')}>Click Me</Button>
<Button onPress={() => console.log('Primary action')} color="primary" variant="solid">
<Button onClick={() => alert('Button clicked!')}>Click Me</Button>
<Button onClick={() => console.log('Primary action')} variant="default">
Primary Action
</Button>
</div>
)
}
// 组合示例
export const Combinations: Story = {
render: () => (
<div className="flex flex-col gap-4">
<div className="flex gap-2 flex-wrap">
<Button variant="default" size="sm">
Small Default
</Button>
<Button variant="destructive" size="sm">
Small Destructive
</Button>
<Button variant="outline" size="sm">
Small Outline
</Button>
</div>
<div className="flex gap-2 flex-wrap">
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
</div>
<div className="flex gap-2 flex-wrap">
<Button variant="default" size="lg">
Large Default
</Button>
<Button variant="destructive" size="lg">
Large Destructive
</Button>
<Button variant="outline" size="lg">
Large Outline
</Button>
</div>
</div>
)
}

View File

@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import CopyButton from '../../../src/components/base/CopyButton'
import CopyButton from '../../../src/components/primitives/copyButton'
const meta: Meta<typeof CopyButton> = {
title: 'Base/CopyButton',

View File

@ -1,459 +0,0 @@
import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react'
import { AlertTriangle, CreditCard, Info, Monitor, Settings, Shield } from 'lucide-react'
import { useState } from 'react'
import CustomCollapse, { Accordion, AccordionItem } from '../../../src/components/base/CustomCollapse'
const meta: Meta<typeof CustomCollapse> = {
title: 'Base/CustomCollapse',
component: CustomCollapse,
parameters: {
layout: 'padded'
},
tags: ['autodocs'],
argTypes: {
children: {
control: false,
description: '面板内容'
},
accordionProps: {
control: false,
description: 'Accordion 组件的属性'
},
accordionItemProps: {
control: false,
description: 'AccordionItem 组件的属性'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
// 基础用法
export const Default: Story = {
args: {
accordionItemProps: {
title: '默认折叠面板'
},
children: (
<div className="p-4">
<p></p>
<p></p>
</div>
)
}
}
// 带副标题
export const WithSubtitle: Story = {
args: {
accordionProps: {
defaultExpandedKeys: ['1']
},
accordionItemProps: {
title: '带副标题的折叠面板',
subtitle: <span className="text-sm text-gray-500"></span>
},
children: (
<div className="p-4">
<p></p>
<p> subtitle </p>
</div>
)
}
}
// HeroUI 样式变体
export const VariantLight: Story = {
args: {
accordionProps: {
variant: 'light'
},
accordionItemProps: {
title: 'Light 变体'
},
children: (
<div className="p-4">
<p> HeroUI Light </p>
</div>
)
}
}
export const VariantShadow: Story = {
args: {
accordionProps: {
variant: 'shadow',
className: 'p-2'
},
accordionItemProps: {
title: 'Shadow 变体',
subtitle: '带阴影的面板样式'
},
children: (
<div className="p-4">
<p> HeroUI Shadow </p>
</div>
)
}
}
export const VariantBordered: Story = {
args: {
accordionProps: {
variant: 'bordered'
},
accordionItemProps: {
title: 'Bordered 变体(默认)'
},
children: (
<div className="p-4">
<p> HeroUI Bordered </p>
</div>
)
}
}
export const VariantSplitted: Story = {
args: {
accordionProps: {
variant: 'splitted'
},
accordionItemProps: {
title: 'Splitted 变体'
},
children: (
<div className="p-4">
<p> HeroUI Splitted </p>
</div>
)
}
}
// 富内容标题
export const RichLabel: Story = {
args: {
accordionItemProps: {
title: (
<div className="flex items-center gap-2">
<Settings className="text-default-500" size={20} />
<span></span>
</div>
)
},
children: (
<div className="p-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<span></span>
<Button size="sm" variant="flat">
</Button>
</div>
<div className="flex items-center justify-between">
<span></span>
<Button size="sm" variant="flat">
</Button>
</div>
</div>
</div>
)
}
}
// 带警告提示
export const WithWarning: Story = {
args: {
accordionItemProps: {
title: (
<div className="flex items-center gap-2">
<Monitor className="text-primary" size={20} />
<span></span>
</div>
),
subtitle: (
<p className="flex">
2<span className="text-primary ml-1"></span>
</p>
)
},
children: (
<div className="p-4">
<p className="text-small"></p>
<ul className="list-disc list-inside mt-2 text-small space-y-1">
<li></li>
<li></li>
</ul>
</div>
)
}
}
// 禁用状态
export const Disabled: Story = {
args: {
accordionProps: {
isDisabled: true,
defaultExpandedKeys: ['1']
},
accordionItemProps: {
title: '禁用的折叠面板'
},
children: (
<div className="p-4">
<p></p>
</div>
)
}
}
// 受控模式
export const ControlledMode: Story = {
render: function ControlledMode() {
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set(['1']))
return (
<div className="space-y-4">
<div className="flex gap-2">
<Button size="sm" onPress={() => setSelectedKeys(new Set(['1']))} color="primary">
</Button>
<Button size="sm" onPress={() => setSelectedKeys(new Set())} color="default">
</Button>
</div>
<CustomCollapse
accordionProps={{
selectedKeys,
onSelectionChange: (keys) => {
if (keys !== 'all') {
setSelectedKeys(keys as Set<string>)
}
}
}}
accordionItemProps={{
title: '受控的折叠面板'
}}>
<div className="p-4">
<p></p>
<p></p>
</div>
</CustomCollapse>
<div className="text-sm text-gray-600">{selectedKeys.size > 0 ? '展开' : '收起'}</div>
</div>
)
}
}
// 多个单面板组合
export const MultipleSinglePanels: Story = {
render: () => (
<div className="space-y-4">
<CustomCollapse accordionProps={{ defaultExpandedKeys: ['1'] }} accordionItemProps={{ title: '第一个面板' }}>
<div className="p-4">
<p></p>
</div>
</CustomCollapse>
<CustomCollapse
accordionItemProps={{
title: '第二个面板',
subtitle: '带副标题'
}}>
<div className="p-4">
<p></p>
</div>
</CustomCollapse>
<CustomCollapse accordionProps={{ isDisabled: true }} accordionItemProps={{ title: '第三个面板(禁用)' }}>
<div className="p-4">
<p></p>
</div>
</CustomCollapse>
</div>
)
}
// 使用原生 HeroUI Accordion 的多面板示例
export const NativeAccordionMultiple: Story = {
render: () => (
<div className="max-w-lg">
<h3 className="text-lg font-medium mb-4"> HeroUI Accordion </h3>
<Accordion variant="shadow" className="p-2 flex flex-col gap-1" defaultExpandedKeys={['1']}>
<AccordionItem
key="1"
title="连接的设备"
startContent={<Monitor className="text-primary" size={20} />}
subtitle={
<p className="flex">
2<span className="text-primary ml-1"></span>
</p>
}>
<div className="p-4">
<p className="text-small"></p>
</div>
</AccordionItem>
<AccordionItem
key="2"
title="应用权限"
startContent={<Shield className="text-default-500" size={20} />}
subtitle="3个应用有读取权限">
<div className="p-4">
<p className="text-small"></p>
</div>
</AccordionItem>
<AccordionItem
key="3"
title="待办任务"
startContent={<Info className="text-warning" size={20} />}
subtitle={<span className="text-warning"></span>}>
<div className="p-4">
<p className="text-small"></p>
</div>
</AccordionItem>
<AccordionItem
key="4"
title={
<p className="flex gap-1 items-center">
<span className="text-default-400 text-small">*4812</span>
</p>
}
startContent={<CreditCard className="text-danger" size={20} />}
subtitle={<span className="text-danger"></span>}>
<div className="p-4">
<p className="text-small text-danger"></p>
</div>
</AccordionItem>
</Accordion>
</div>
)
}
// 富内容面板
export const RichContent: Story = {
args: {
accordionItemProps: {
title: (
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<Info className="text-default-500" size={20} />
<span></span>
</div>
<div className="flex gap-2" onClick={(e) => e.stopPropagation()}>
<Button size="sm" variant="flat" color="primary">
</Button>
<Button size="sm" variant="flat">
</Button>
</div>
</div>
)
},
children: (
<div className="p-4 space-y-4">
<div>
<h4 className="font-medium mb-2"></h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1"></label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="请输入名称"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1"></label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-md">
<option></option>
<option> A</option>
<option> B</option>
</select>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1"></label>
<textarea className="w-full px-3 py-2 border border-gray-300 rounded-md" rows={3} placeholder="请输入描述" />
</div>
</div>
)
}
}
// 自定义样式
export const CustomStyles: Story = {
args: {
accordionProps: {
style: {
backgroundColor: 'rgba(255, 193, 7, 0.1)',
borderColor: 'var(--color-warning)'
}
},
accordionItemProps: {
title: (
<div className="flex items-center gap-2">
<AlertTriangle className="text-warning" size={16} />
<span></span>
</div>
)
},
children: (
<div className="p-4 bg-warning-50 dark:bg-warning-900/20">
<p className="text-warning-800 dark:text-warning-200"></p>
</div>
)
}
}
// 原生 HeroUI Accordion 多面板受控模式
export const NativeAccordionControlled: Story = {
render: function NativeAccordionControlled() {
const [activeKeys, setActiveKeys] = useState<Set<string>>(new Set(['1']))
return (
<div className="space-y-4">
<div className="flex gap-2">
<Button size="sm" onPress={() => setActiveKeys(new Set(['1', '2', '3']))} color="primary">
</Button>
<Button size="sm" onPress={() => setActiveKeys(new Set())} color="default">
</Button>
<Button size="sm" onPress={() => setActiveKeys(new Set(['2']))} color="default">
</Button>
</div>
<Accordion
selectedKeys={activeKeys}
onSelectionChange={(keys) => {
if (keys !== 'all') {
setActiveKeys(keys as Set<string>)
}
}}>
<AccordionItem key="1" title="受控面板 1">
<div className="p-4">
<p></p>
</div>
</AccordionItem>
<AccordionItem key="2" title="受控面板 2">
<div className="p-4">
<p></p>
</div>
</AccordionItem>
<AccordionItem key="3" title="受控面板 3">
<div className="p-4">
<p></p>
</div>
</AccordionItem>
</Accordion>
<div className="text-sm text-gray-600">
{activeKeys.size > 0 ? Array.from(activeKeys).join(', ') : '无'}
</div>
</div>
)
}
}

View File

@ -2,7 +2,7 @@ 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'
import CustomTag from '../../../src/components/primitives/customTag'
const meta: Meta<typeof CustomTag> = {
title: 'Base/CustomTag',

View File

@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import DividerWithText from '../../../src/components/base/DividerWithText'
import DividerWithText from '../../../src/components/primitives/dividerWithText'
const meta: Meta<typeof DividerWithText> = {
title: 'Base/DividerWithText',

View File

@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import EmojiAvatar from '../../../src/components/base/Avatar/EmojiAvatar'
import EmojiAvatar from '../../../src/components/primitives/Avatar/EmojiAvatar'
const meta: Meta<typeof EmojiAvatar> = {
title: 'Display/EmojiAvatar',

View File

@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import EmojiIcon from '../../../src/components/base/EmojiIcon'
import EmojiIcon from '../../../src/components/primitives/emojiIcon'
const meta: Meta<typeof EmojiIcon> = {
title: 'Base/EmojiIcon',

View File

@ -2,8 +2,8 @@ import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react'
import { useState } from 'react'
import type { CustomFallbackProps } from '../../../src/components/base/ErrorBoundary'
import { ErrorBoundary } from '../../../src/components/base/ErrorBoundary'
import type { CustomFallbackProps } from '../../../src/components/primitives/ErrorBoundary'
import { ErrorBoundary } from '../../../src/components/primitives/ErrorBoundary'
// 错误组件 - 用于触发错误
const ThrowErrorComponent = ({ shouldThrow = false, errorMessage = '这是一个模拟错误' }) => {

View File

@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import IndicatorLight from '../../../src/components/base/IndicatorLight'
import IndicatorLight from '../../../src/components/primitives/indicatorLight'
const meta: Meta<typeof IndicatorLight> = {
title: 'Base/IndicatorLight',

View File

@ -2,7 +2,7 @@ import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react'
import { useState } from 'react'
import Spinner from '../../../src/components/base/Spinner'
import Spinner from '../../../src/components/primitives/spinner'
const meta: Meta<typeof Spinner> = {
title: 'Base/Spinner',

View File

@ -1,176 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react'
import { ErrorTag, InfoTag, StatusTag, SuccessTag, WarnTag } from '../../../src/components/base/StatusTag'
const meta: Meta<typeof StatusTag> = {
title: 'Base/StatusTag',
component: StatusTag,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
type: {
control: { type: 'select' },
options: ['success', 'error', 'warning', 'info']
},
iconSize: { control: { type: 'range', min: 10, max: 24, step: 1 } },
message: { control: 'text' },
color: { control: 'color' }
}
}
export default meta
type Story = StoryObj<typeof meta>
// Default
export const Default: Story = {
args: {
type: 'success',
message: 'Success'
}
}
// All Types
export const AllTypes: Story = {
render: () => (
<div className="flex flex-col gap-3">
<StatusTag type="success" message="Success message" />
<StatusTag type="error" message="Error message" />
<StatusTag type="warning" message="Warning message" />
<StatusTag type="info" message="Info message" />
</div>
)
}
// Convenience Components
export const ConvenienceComponents: Story = {
render: () => (
<div className="flex flex-col gap-3">
<SuccessTag message="Operation completed" />
<ErrorTag message="Operation failed" />
<WarnTag message="Please check this" />
<InfoTag message="Additional information" />
</div>
)
}
// Different Icon Sizes
export const IconSizes: Story = {
render: () => (
<div className="space-y-4">
<div className="flex items-center gap-4">
<StatusTag type="success" iconSize={10} message="Small icon" />
<StatusTag type="success" iconSize={14} message="Default icon" />
<StatusTag type="success" iconSize={18} message="Large icon" />
<StatusTag type="success" iconSize={24} message="Extra large icon" />
</div>
<div className="flex items-center gap-4">
<ErrorTag iconSize={10} message="Small icon" />
<ErrorTag iconSize={14} message="Default icon" />
<ErrorTag iconSize={18} message="Large icon" />
<ErrorTag iconSize={24} message="Extra large icon" />
</div>
</div>
)
}
// Custom Colors
export const CustomColors: Story = {
render: () => (
<div className="flex flex-col gap-3">
<StatusTag type="success" message="Custom purple" color="#8B5CF6" />
<StatusTag type="error" message="Custom blue" color="#3B82F6" />
<StatusTag type="warning" message="Custom green" color="#10B981" />
<StatusTag type="info" message="Custom pink" color="#EC4899" />
</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">Validation Error</h3>
<p className="mb-3 text-sm text-gray-600">Please fix the following issues:</p>
<ErrorTag message="Invalid email format" />
</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="Database connected" />
<WarnTag message="High memory usage" />
<ErrorTag message="Email service down" />
<InfoTag message="Last backup: 2 hours ago" />
</div>
</div>
</div>
)
}
// Use Cases
export const UseCases: Story = {
render: () => (
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<h4 className="font-medium">Success States</h4>
<div className="space-y-2">
<SuccessTag message="Saved" />
<SuccessTag message="Published" />
<SuccessTag message="Deployed" />
<SuccessTag message="Verified" />
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">Error States</h4>
<div className="space-y-2">
<ErrorTag message="Failed" />
<ErrorTag message="Timeout" />
<ErrorTag message="Not found" />
<ErrorTag message="Access denied" />
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">Warning States</h4>
<div className="space-y-2">
<WarnTag message="Deprecated" />
<WarnTag message="Limited" />
<WarnTag message="Expiring soon" />
<WarnTag message="Low balance" />
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">Info States</h4>
<div className="space-y-2">
<InfoTag message="New" />
<InfoTag message="Beta" />
<InfoTag message="Preview" />
<InfoTag message="Optional" />
</div>
</div>
</div>
)
}
// Long Messages
export const LongMessages: Story = {
render: () => (
<div className="max-w-md space-y-3">
<SuccessTag message="Your request has been successfully processed and saved to the database" />
<ErrorTag message="Unable to connect to the server. Please check your network connection and try again" />
<WarnTag message="This feature will be deprecated in the next major version. Please migrate to the new API" />
<InfoTag message="Additional information about this feature can be found in the documentation at docs.example.com" />
</div>
)
}

View File

@ -1,383 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react'
import TextBadge from '../../../src/components/base/TextBadge'
const meta: Meta<typeof TextBadge> = {
title: 'Base/TextBadge',
component: TextBadge,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
text: {
control: 'text',
description: '徽章显示的文字'
},
style: {
control: false,
description: '自定义样式对象'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
text: '新'
}
}
export const ShortText: Story = {
args: {
text: 'V2'
}
}
export const LongText: Story = {
args: {
text: '热门推荐'
}
}
export const Numbers: Story = {
args: {
text: '99+'
}
}
export const Status: Story = {
args: {
text: '已完成'
}
}
export const Version: Story = {
args: {
text: 'v1.2.0'
}
}
export const CustomStyle: Story = {
args: {
text: '自定义',
style: {
backgroundColor: '#10b981',
color: 'white',
fontSize: '11px'
}
}
}
export const CustomClassName: Story = {
args: {
text: '特殊样式',
className:
'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 border border-purple-200 dark:border-purple-700'
}
}
export const ColorVariations: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="flex flex-wrap gap-3">
<TextBadge text="默认蓝色" />
<TextBadge text="绿色" className="bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" />
<TextBadge text="红色" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
<TextBadge text="黄色" className="bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400" />
<TextBadge text="紫色" className="bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400" />
<TextBadge text="灰色" className="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400" />
</div>
</div>
)
}
export const StatusBadges: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-2">
<TextBadge text="待处理" className="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400" />
<TextBadge text="进行中" className="bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400" />
<TextBadge text="已完成" className="bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" />
<TextBadge text="已取消" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-2">
<TextBadge text="低" className="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400" />
<TextBadge text="中" />
<TextBadge text="高" className="bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400" />
<TextBadge text="紧急" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-2">
<TextBadge text="功能" />
<TextBadge text="修复" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
<TextBadge text="优化" className="bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" />
<TextBadge
text="文档"
className="bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400"
/>
</div>
</div>
</div>
</div>
)
}
export const InUserInterface: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
{/* 导航菜单 */}
<div>
<h4 className="font-medium mb-2"></h4>
<nav className="flex gap-4">
<div className="flex items-center gap-2">
<span></span>
</div>
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="新" />
</div>
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="5" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
</div>
<div className="flex items-center gap-2">
<span></span>
</div>
</nav>
</div>
{/* 卡片列表 */}
<div>
<h4 className="font-medium mb-2"></h4>
<div className="space-y-3">
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-start justify-between">
<div className="flex-1">
<h5 className="font-medium">React 18 </h5>
<p className="text-sm text-gray-500 mt-1"> React 18 ...</p>
</div>
<div className="flex gap-2 ml-4">
<TextBadge text="前端" />
<TextBadge
text="推荐"
className="bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400"
/>
</div>
</div>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-start justify-between">
<div className="flex-1">
<h5 className="font-medium">Node.js </h5>
<p className="text-sm text-gray-500 mt-1"> Node.js ...</p>
</div>
<div className="flex gap-2 ml-4">
<TextBadge text="后端" />
<TextBadge text="热门" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
</div>
</div>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-start justify-between">
<div className="flex-1">
<h5 className="font-medium">TypeScript </h5>
<p className="text-sm text-gray-500 mt-1">TypeScript ...</p>
</div>
<div className="flex gap-2 ml-4">
<TextBadge text="TypeScript" />
<TextBadge text="新" />
</div>
</div>
</div>
</div>
</div>
{/* 用户列表 */}
<div>
<h4 className="font-medium mb-2"></h4>
<div className="space-y-2">
<div className="flex items-center justify-between p-3 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm">
</div>
<div>
<p className="font-medium"></p>
<p className="text-sm text-gray-500"></p>
</div>
</div>
<div className="flex gap-2">
<TextBadge
text="管理员"
className="bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400"
/>
<TextBadge
text="在线"
className="bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400"
/>
</div>
</div>
<div className="flex items-center justify-between p-3 border border-gray-200 dark:border-gray-700 rounded">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center text-sm">
</div>
<div>
<p className="font-medium"></p>
<p className="text-sm text-gray-500"></p>
</div>
</div>
<div className="flex gap-2">
<TextBadge text="成员" />
<TextBadge text="离线" className="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400" />
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export const VersionTags: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-2">
<TextBadge text="v1.0.0" />
<TextBadge text="v1.1.0" />
<TextBadge text="v2.0.0-beta" />
<TextBadge text="v2.1.0" className="bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" />
<TextBadge text="latest" className="bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400" />
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-2">
<TextBadge text="开发" className="bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400" />
<TextBadge
text="测试"
className="bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400"
/>
<TextBadge
text="预发布"
className="bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400"
/>
<TextBadge text="生产" className="bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" />
</div>
</div>
</div>
</div>
)
}
export const NumberBadges: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="3" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
</div>
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="12" className="bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="99+" className="bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400" />
</div>
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<span>访</span>
<TextBadge text="1.2K" />
</div>
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="856" />
</div>
<div className="flex items-center gap-2">
<span></span>
<TextBadge text="234" />
</div>
</div>
</div>
</div>
</div>
)
}
export const SizeVariations: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="flex items-center gap-4">
<TextBadge text="超小" style={{ fontSize: '10px', padding: '1px 4px' }} />
<TextBadge text="小" style={{ fontSize: '11px', padding: '2px 6px' }} />
<TextBadge text="默认" />
<TextBadge text="大" style={{ fontSize: '14px', padding: '4px 8px' }} />
<TextBadge text="超大" style={{ fontSize: '16px', padding: '6px 12px' }} />
</div>
</div>
)
}
export const OutlineBadges: Story = {
render: () => (
<div className="space-y-6">
<h3 className="text-lg font-medium"></h3>
<div className="flex flex-wrap gap-3">
<TextBadge
text="边框"
className="bg-transparent border border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400"
/>
<TextBadge
text="绿色边框"
className="bg-transparent border border-green-600 text-green-600 dark:border-green-400 dark:text-green-400"
/>
<TextBadge
text="红色边框"
className="bg-transparent border border-red-600 text-red-600 dark:border-red-400 dark:text-red-400"
/>
<TextBadge
text="虚线边框"
className="bg-transparent border border-dashed border-purple-600 text-purple-600 dark:border-purple-400 dark:text-purple-400"
/>
</div>
</div>
)
}

View File

@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import Ellipsis from '../../../src/components/display/Ellipsis'
import Ellipsis from '../../../src/components/composites/Ellipsis'
const meta = {
title: 'Display/Ellipsis',

View File

@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import ExpandableText from '../../../src/components/display/ExpandableText'
import ExpandableText from '../../../src/components/composites/ExpandableText'
const meta: Meta<typeof ExpandableText> = {
title: 'Display/ExpandableText',

View File

@ -16,7 +16,7 @@ import {
} from 'lucide-react'
import { action } from 'storybook/actions'
import ListItem from '../../../src/components/display/ListItem'
import ListItem from '../../../src/components/composites/ListItem'
const meta: Meta<typeof ListItem> = {
title: 'Display/ListItem',

View File

@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import MaxContextCount from '../../../src/components/display/MaxContextCount'
import MaxContextCount from '../../../src/components/composites/MaxContextCount'
const meta: Meta<typeof MaxContextCount> = {
title: 'Display/MaxContextCount',

View File

@ -2,7 +2,7 @@ import { Button } from '@heroui/react'
import type { Meta, StoryObj } from '@storybook/react'
import { useEffect, useMemo, useState } from 'react'
import ThinkingEffect from '../../../src/components/display/ThinkingEffect'
import ThinkingEffect from '../../../src/components/composites/ThinkingEffect'
const meta: Meta<typeof ThinkingEffect> = {
title: 'Display/ThinkingEffect',

View File

@ -1,8 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { action } from 'storybook/actions'
import type { LanguageConfig } from '../../../src/components/interactive/CodeEditor'
import CodeEditor, { getCmThemeByName, getCmThemeNames } from '../../../src/components/interactive/CodeEditor'
import type { LanguageConfig } from '../../../src/components/composites/CodeEditor'
import CodeEditor, { getCmThemeByName, getCmThemeNames } from '../../../src/components/composites/CodeEditor'
// 示例语言配置 - 为 Storybook 提供更丰富的语言支持演示
const exampleLanguageConfig: LanguageConfig = {

View File

@ -1,7 +1,7 @@
import type { Meta } from '@storybook/react'
import { useState } from 'react'
import Selector from '../../../src/components/base/Selector'
import Selector from '../../../src/components/primitives/Selector'
const meta: Meta<typeof Selector> = {
title: 'Interactive/Selector',

View File

@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import clsx from 'clsx'
import { useMemo, useState } from 'react'
import { Sortable } from '../../../src/components/interactive/Sortable'
import { Sortable } from '../../../src/components/composites/Sortable'
import { useDndReorder } from '../../../src/hooks'
type ExampleItem = { id: number; label: string }

View File

@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { useState } from 'react'
import HorizontalScrollContainer from '../../../src/components/layout/HorizontalScrollContainer'
import HorizontalScrollContainer from '../../../src/components/composites/HorizontalScrollContainer'
const meta: Meta<typeof HorizontalScrollContainer> = {
title: 'Layout/HorizontalScrollContainer',

View File

@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import Scrollbar from '../../../src/components/layout/Scrollbar'
import Scrollbar from '../../../src/components/composites/Scrollbar'
const meta: Meta<typeof Scrollbar> = {
title: 'Layout/Scrollbar',

View File

@ -1,8 +1,7 @@
import { cn } from '@cherrystudio/ui'
import { Button, type ButtonProps } from '@cherrystudio/ui'
import { Button, cn } from '@cherrystudio/ui'
import React, { memo } from 'react'
interface ActionIconButtonProps extends Omit<ButtonProps, 'ref'> {
interface ActionIconButtonProps extends Omit<React.ComponentProps<'button'>, 'ref'> {
icon: React.ReactNode
active?: boolean
}
@ -13,19 +12,17 @@ interface ActionIconButtonProps extends Omit<ButtonProps, 'ref'> {
const ActionIconButton: React.FC<ActionIconButtonProps> = ({ icon, active = false, className, ...props }) => {
return (
<Button
size="sm"
variant="light"
radius="full"
isIconOnly
startContent={icon}
size="icon-sm"
variant="ghost"
className={cn(
'flex cursor-pointer flex-row items-center justify-center border-none p-0 text-base transition-all duration-300 ease-in-out [&_.anticon]:text-icon [&_.icon-a-addchat]:mb-[-2px] [&_.icon-a-addchat]:text-lg [&_.icon]:text-icon [&_.iconfont]:text-icon [&_.lucide]:text-icon',
'flex cursor-pointer flex-row items-center justify-center rounded-full border-none p-0 text-base transition-all duration-300 ease-in-out [&_.anticon]:text-icon [&_.icon-a-addchat]:mb-[-2px] [&_.icon-a-addchat]:text-lg [&_.icon]:text-icon [&_.iconfont]:text-icon [&_.lucide]:text-icon',
active &&
'[&_.anticon]:text-primary! [&_.icon]:text-primary! [&_.iconfont]:text-primary! [&_.lucide]:text-primary!',
className
)}
{...props}
/>
{...props}>
{icon}
</Button>
)
}

View File

@ -89,32 +89,24 @@ const HtmlArtifactsCard: FC<Props> = ({ html, onSave, isStreaming = false }) =>
</TerminalContent>
</TerminalPreview>
<ButtonContainer>
<Button startContent={<CodeOutlined />} onPress={() => setIsPopupOpen(true)} color="primary">
<Button onClick={() => setIsPopupOpen(true)}>
<CodeOutlined />
{t('chat.artifacts.button.preview')}
</Button>
</ButtonContainer>
</>
) : (
<ButtonContainer>
<Button
startContent={<CodeOutlined />}
onPress={() => setIsPopupOpen(true)}
variant="light"
isDisabled={!hasContent}>
<Button onClick={() => setIsPopupOpen(true)} variant="ghost" disabled={!hasContent}>
<CodeOutlined />
{t('chat.artifacts.button.preview')}
</Button>
<Button
startContent={<LinkIcon size={14} />}
onPress={handleOpenExternal}
variant="light"
isDisabled={!hasContent}>
<Button onClick={handleOpenExternal} variant="ghost" disabled={!hasContent}>
<LinkIcon size={14} />
{t('chat.artifacts.button.openExternal')}
</Button>
<Button
startContent={<DownloadIcon size={14} />}
onPress={handleDownload}
variant="light"
isDisabled={!hasContent}>
<Button onClick={handleDownload} variant="ghost" disabled={!hasContent}>
<DownloadIcon size={14} />
{t('code_block.download.label')}
</Button>
</ButtonContainer>

View File

@ -85,23 +85,23 @@ const HtmlArtifactsPopup: React.FC<HtmlArtifactsPopupProps> = ({ open, title, ht
<ViewControls onDoubleClick={(e) => e.stopPropagation()}>
<ViewButton
size="sm"
color={viewMode === 'split' ? 'primary' : 'default'}
startContent={<SquareSplitHorizontal size={14} />}
onPress={() => setViewMode('split')}>
variant={viewMode === 'split' ? 'default' : 'secondary'}
onClick={() => setViewMode('split')}>
<SquareSplitHorizontal size={14} />
{t('html_artifacts.split')}
</ViewButton>
<ViewButton
size="sm"
color={viewMode === 'code' ? 'primary' : 'default'}
startContent={<Code size={14} />}
onPress={() => setViewMode('code')}>
variant={viewMode === 'code' ? 'default' : 'secondary'}
onClick={() => setViewMode('code')}>
<Code size={14} />
{t('html_artifacts.code')}
</ViewButton>
<ViewButton
size="sm"
color={viewMode === 'preview' ? 'primary' : 'default'}
startContent={<Eye size={14} />}
onPress={() => setViewMode('preview')}>
variant={viewMode === 'preview' ? 'default' : 'secondary'}
onClick={() => setViewMode('preview')}>
<Eye size={14} />
{t('html_artifacts.preview')}
</ViewButton>
</ViewControls>
@ -127,17 +127,17 @@ const HtmlArtifactsPopup: React.FC<HtmlArtifactsPopupProps> = ({ open, title, ht
]
}}>
<Tooltip content={t('html_artifacts.capture.label')} closeDelay={0}>
<Button variant="light" startContent={<Camera size={16} />} isIconOnly className="nodrag" />
<Button variant="ghost" size="icon" className="nodrag">
<Camera size={16} />
</Button>
</Tooltip>
</Dropdown>
<Button
onPress={() => setIsFullscreen(!isFullscreen)}
variant="light"
startContent={isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
isIconOnly
className="nodrag"
/>
<Button onPress={onClose} variant="light" startContent={<X size={16} />} isIconOnly className="nodrag" />
<Button onClick={() => setIsFullscreen(!isFullscreen)} variant="ghost" size="icon" className="nodrag">
{isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
</Button>
<Button onClick={onClose} variant="ghost" size="icon" className="nodrag">
<X size={16} />
</Button>
</HeaderRight>
</ModalHeader>
)
@ -165,7 +165,7 @@ const HtmlArtifactsPopup: React.FC<HtmlArtifactsPopupProps> = ({ open, title, ht
/>
<ToolbarWrapper>
<Tooltip content={t('code_block.edit.save.label')} closeDelay={0}>
<ToolbarButton radius="full" size="lg" isIconOnly onPress={handleSave}>
<ToolbarButton className="rounded-full" size="icon-lg" onClick={handleSave}>
{saved ? (
<Check size={16} color="var(--color-status-success)" />
) : (

View File

@ -1,4 +1,4 @@
import { Button } from '@heroui/react'
import { Button } from '@cherrystudio/ui'
import { CheckIcon, XIcon } from 'lucide-react'
import type { FC } from 'react'
import { createPortal } from 'react-dom'
@ -28,11 +28,13 @@ const ConfirmDialog: FC<Props> = ({ x, y, message, onConfirm, onCancel }) => {
<div className="flex min-w-[160px] items-center rounded-lg border border-[var(--color-border)] bg-[var(--color-background)] p-3 shadow-[0_4px_12px_rgba(0,0,0,0.15)]">
<div className="mr-2 text-sm leading-[1.4]">{message}</div>
<div className="flex justify-center gap-2">
<Button onPress={onCancel} radius="full" className="h-6 w-6 min-w-0 p-1" color="danger">
<XIcon className="text-danger-foreground" size={16} />
<Button onClick={onCancel} className="h-6 w-6 min-w-0 rounded-full p-1" variant="destructive">
<XIcon size={16} />
</Button>
<Button onPress={onConfirm} radius="full" className="h-6 w-6 min-w-0 p-1" color="success">
<CheckIcon className="text-success-foreground" size={16} />
<Button
onClick={onConfirm}
className="h-6 w-6 min-w-0 rounded-full bg-green-500 p-1 text-white hover:bg-green-600">
<CheckIcon size={16} />
</Button>
</div>
</div>

View File

@ -365,8 +365,7 @@ export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
{showUserToggle && (
<Tooltip placement="bottom" content={t('button.includes_user_questions')} delay={800}>
<ActionIconButton
onPress={userOutlinedButtonOnClick}
isIconOnly
onClick={userOutlinedButtonOnClick}
icon={
<User size={18} style={{ color: includeUser ? 'var(--color-link)' : 'var(--color-icon)' }} />
}
@ -375,7 +374,7 @@ export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
)}
<Tooltip placement="bottom" content={t('button.case_sensitive')} delay={800}>
<ActionIconButton
onPress={caseSensitiveButtonOnClick}
onClick={caseSensitiveButtonOnClick}
icon={
<CaseSensitive
size={18}
@ -386,7 +385,7 @@ export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
</Tooltip>
<Tooltip placement="bottom" content={t('button.whole_word')} delay={800}>
<ActionIconButton
onPress={wholeWordButtonOnClick}
onClick={wholeWordButtonOnClick}
icon={
<WholeWord size={18} style={{ color: isWholeWord ? 'var(--color-link)' : 'var(--color-icon)' }} />
}
@ -408,16 +407,16 @@ export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
</SearchResults>
<ToolBar>
<ActionIconButton
onPress={prevButtonOnClick}
isDisabled={allRanges.length === 0}
onClick={prevButtonOnClick}
disabled={allRanges.length === 0}
icon={<ChevronUp size={18} />}
/>
<ActionIconButton
onPress={nextButtonOnClick}
isDisabled={allRanges.length === 0}
onClick={nextButtonOnClick}
disabled={allRanges.length === 0}
icon={<ChevronDown size={18} />}
/>
<ActionIconButton onPress={closeButtonOnClick} icon={<X size={18} />} />
<ActionIconButton onClick={closeButtonOnClick} icon={<X size={18} />} />
</ToolBar>
</SearchBarContainer>
</NarrowLayout>

View File

@ -24,10 +24,10 @@ const DefaultFallback: ComponentType<FallbackProps> = (props: FallbackProps): Re
type="error"
action={
<Space>
<Button size="sm" onPress={debug}>
<Button size="sm" onClick={debug}>
{t('error.boundary.default.devtools')}
</Button>
<Button size="sm" onPress={reload}>
<Button size="sm" onClick={reload}>
{t('error.boundary.default.reload')}
</Button>
</Space>

View File

@ -22,7 +22,7 @@ const ExpandableText = ({
const button = useMemo(() => {
return (
<Button variant="light" onPress={toggleExpand} className="self-end">
<Button variant="ghost" onClick={toggleExpand} className="self-end">
{isExpanded ? t('common.collapse') : t('common.expand')}
</Button>
)

View File

@ -78,12 +78,11 @@ const InputEmbeddingDimension = ({
<Button
role="button"
aria-label="Get embedding dimension"
isDisabled={disabled || loading}
onPress={handleFetchDimension}
size="sm"
startContent={<RefreshIcon size={16} className={loading ? 'animation-rotate' : ''} />}
isIconOnly
/>
disabled={disabled || loading}
onClick={handleFetchDimension}
size="icon-sm">
<RefreshIcon size={16} className={loading ? 'animation-rotate' : ''} />
</Button>
</Tooltip>
</Space.Compact>
)

View File

@ -196,18 +196,17 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe
<Button
className="inline-flex"
size="sm"
variant="light"
onPress={() => handleRestore(record.fileName)}
isDisabled={restoring || deleting}>
variant="ghost"
onClick={() => handleRestore(record.fileName)}
disabled={restoring || deleting}>
{t('settings.data.local.backup.manager.restore.text')}
</Button>
<Button
className="inline-flex"
size="sm"
variant="light"
color="danger"
onPress={() => handleDeleteSingle(record.fileName)}
isDisabled={deleting || restoring}>
variant="ghost"
onClick={() => handleDeleteSingle(record.fileName)}
disabled={deleting || restoring}>
{t('settings.data.local.backup.manager.delete.text')}
</Button>
</Flex>
@ -232,19 +231,19 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe
transitionName="animation-move-down"
classNames={{ footer: 'flex justify-end gap-1' }}
footer={[
<Button key="refresh" startContent={<ReloadOutlined />} onPress={fetchBackupFiles} isDisabled={loading}>
<Button key="refresh" onClick={fetchBackupFiles} disabled={loading}>
<ReloadOutlined />
{t('settings.data.local.backup.manager.refresh')}
</Button>,
<Button
key="delete"
color="danger"
startContent={<DeleteOutlined />}
onPress={handleDeleteSelected}
isDisabled={selectedRowKeys.length === 0 || deleting}
isLoading={deleting}>
variant="destructive"
onClick={handleDeleteSelected}
disabled={selectedRowKeys.length === 0 || deleting}>
<DeleteOutlined />
{t('settings.data.local.backup.manager.delete.selected')} ({selectedRowKeys.length})
</Button>,
<Button key="close" onPress={onClose}>
<Button key="close" onClick={onClose}>
{t('common.close')}
</Button>
]}>

View File

@ -35,10 +35,10 @@ export function LocalBackupModal({
onCancel={handleCancel}
classNames={{ footer: 'flex justify-end gap-1' }}
footer={[
<Button key="back" onPress={handleCancel}>
<Button key="back" onClick={handleCancel}>
{t('common.cancel')}
</Button>,
<Button key="submit" color="primary" isLoading={backuping} onPress={handleBackup}>
<Button key="submit" disabled={backuping} onClick={handleBackup}>
{t('common.confirm')}
</Button>
]}>

View File

@ -130,7 +130,7 @@ const GoogleLoginTip = ({
banner
onClose={handleClose}
action={
<Button color="primary" size="sm" onPress={openGoogleMinApp}>
<Button color="primary" size="sm" onClick={openGoogleMinApp}>
{t('common.open')} Google
</Button>
}

View File

@ -24,13 +24,9 @@ const ModelSelectButton = ({ model, onSelectModel, modelFilter, noTooltip, toolt
const button = useMemo(() => {
return (
<Button
startContent={<ModelAvatar model={model} size={22} />}
variant="light"
radius="full"
isIconOnly
onPress={onClick}
/>
<Button variant="ghost" className="rounded-full" size="icon" onClick={onClick}>
<ModelAvatar model={model} size={22} />
</Button>
)
}, [model, onClick])

Some files were not shown because too many files have changed in this diff Show More