mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-05 12:29:44 +08:00
chore(ui): update package.json and migration status files
- Reformatted keywords and files array in package.json for better readability. - Updated migration status to reflect the migration of additional components, increasing the total migrated count to 46 and reducing pending migrations to 190. - Added new components to the migration status table, including ErrorBoundary and ProviderAvatar, while removing deprecated components like ErrorTag, SuccessTag, and WarnTag.
This commit is contained in:
parent
ffe897d58c
commit
8cc6b08831
@ -49,9 +49,9 @@ function MyComponent() {
|
|||||||
## 迁移概览
|
## 迁移概览
|
||||||
|
|
||||||
- **总组件数**: 236
|
- **总组件数**: 236
|
||||||
- **已迁移**: 43
|
- **已迁移**: 46
|
||||||
- **已重构**: 0
|
- **已重构**: 2
|
||||||
- **待迁移**: 193
|
- **待迁移**: 190
|
||||||
|
|
||||||
## 组件状态表
|
## 组件状态表
|
||||||
|
|
||||||
@ -62,12 +62,11 @@ function MyComponent() {
|
|||||||
| | CustomTag | ✅ | ❌ | 自定义标签 |
|
| | CustomTag | ✅ | ❌ | 自定义标签 |
|
||||||
| | DividerWithText | ✅ | ❌ | 带文本的分隔线 |
|
| | DividerWithText | ✅ | ❌ | 带文本的分隔线 |
|
||||||
| | EmojiIcon | ✅ | ❌ | 表情图标 |
|
| | EmojiIcon | ✅ | ❌ | 表情图标 |
|
||||||
| | ErrorTag | ✅ | ❌ | 错误标签 |
|
| | ErrorBoundary | ✅ | ❌ | 错误边界 (通过 props 解耦) |
|
||||||
|
| | StatusTag | ✅ | ✅ | 统一状态标签(合并了 ErrorTag、SuccessTag、WarnTag、InfoTag)|
|
||||||
| | IndicatorLight | ✅ | ❌ | 指示灯 |
|
| | IndicatorLight | ✅ | ❌ | 指示灯 |
|
||||||
| | Spinner | ✅ | ❌ | 加载动画 |
|
| | Spinner | ✅ | ❌ | 加载动画 |
|
||||||
| | SuccessTag | ✅ | ❌ | 成功标签 |
|
|
||||||
| | TextBadge | ✅ | ❌ | 文本徽标 |
|
| | TextBadge | ✅ | ❌ | 文本徽标 |
|
||||||
| | WarnTag | ✅ | ❌ | 警告标签 |
|
|
||||||
| | CustomCollapse | ✅ | ❌ | 自定义折叠面板 |
|
| | CustomCollapse | ✅ | ❌ | 自定义折叠面板 |
|
||||||
| **display** | | | | 显示组件 |
|
| **display** | | | | 显示组件 |
|
||||||
| | Ellipsis | ✅ | ❌ | 文本省略 |
|
| | Ellipsis | ✅ | ❌ | 文本省略 |
|
||||||
@ -76,6 +75,7 @@ function MyComponent() {
|
|||||||
| | EmojiAvatar | ✅ | ❌ | 表情头像 |
|
| | EmojiAvatar | ✅ | ❌ | 表情头像 |
|
||||||
| | ListItem | ✅ | ❌ | 列表项 |
|
| | ListItem | ✅ | ❌ | 列表项 |
|
||||||
| | MaxContextCount | ✅ | ❌ | 最大上下文数显示 |
|
| | MaxContextCount | ✅ | ❌ | 最大上下文数显示 |
|
||||||
|
| | ProviderAvatar | ✅ | ❌ | 提供者头像 |
|
||||||
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
|
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
|
||||||
| | OGCard | ❌ | ❌ | OG 卡片 |
|
| | OGCard | ❌ | ❌ | OG 卡片 |
|
||||||
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 |
|
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 |
|
||||||
@ -87,22 +87,11 @@ function MyComponent() {
|
|||||||
| | Tab/* | ❌ | ❌ | 标签页 (Redux 依赖) |
|
| | Tab/* | ❌ | ❌ | 标签页 (Redux 依赖) |
|
||||||
| | TopView | ❌ | ❌ | 顶部视图 (window.api 依赖) |
|
| | TopView | ❌ | ❌ | 顶部视图 (window.api 依赖) |
|
||||||
| **icons** | | | | 图标组件 |
|
| **icons** | | | | 图标组件 |
|
||||||
| | CopyIcon | ✅ | ❌ | 复制图标 |
|
| | Icon | ✅ | ✅ | 图标工厂函数和预定义图标(合并了 CopyIcon、DeleteIcon、EditIcon、RefreshIcon、ResetIcon、ToolIcon、VisionIcon、WebSearchIcon、WrapIcon、UnWrapIcon、OcrIcon)|
|
||||||
| | DeleteIcon | ✅ | ❌ | 删除图标 |
|
| | FileIcons | ✅ | ❌ | 文件图标 (FileSvgIcon、FilePngIcon) |
|
||||||
| | EditIcon | ✅ | ❌ | 编辑图标 |
|
|
||||||
| | FileIcons | ✅ | ❌ | 文件图标 (包含 FileSvgIcon、FilePngIcon) |
|
|
||||||
| | ReasoningIcon | ✅ | ❌ | 推理图标 |
|
| | ReasoningIcon | ✅ | ❌ | 推理图标 |
|
||||||
| | RefreshIcon | ✅ | ❌ | 刷新图标 |
|
|
||||||
| | ResetIcon | ✅ | ❌ | 重置图标 |
|
|
||||||
| | SvgSpinners180Ring | ✅ | ❌ | 旋转加载图标 |
|
| | SvgSpinners180Ring | ✅ | ❌ | 旋转加载图标 |
|
||||||
| | ToolsCallingIcon | ✅ | ❌ | 工具调用图标 |
|
| | ToolsCallingIcon | ✅ | ❌ | 工具调用图标 |
|
||||||
| | VisionIcon | ✅ | ❌ | 视觉图标 |
|
|
||||||
| | WebSearchIcon | ✅ | ❌ | 网页搜索图标 |
|
|
||||||
| | WrapIcon | ✅ | ❌ | 换行图标 |
|
|
||||||
| | UnWrapIcon | ✅ | ❌ | 不换行图标 |
|
|
||||||
| | OcrIcon | ✅ | ❌ | OCR 图标 |
|
|
||||||
| | ToolIcon | ✅ | ❌ | 工具图标 |
|
|
||||||
| | Other icons | ❌ | ❌ | 其他图标文件 |
|
|
||||||
| **interactive** | | | | 交互组件 |
|
| **interactive** | | | | 交互组件 |
|
||||||
| | InfoTooltip | ✅ | ❌ | 信息提示 |
|
| | InfoTooltip | ✅ | ❌ | 信息提示 |
|
||||||
| | HelpTooltip | ✅ | ❌ | 帮助提示 |
|
| | HelpTooltip | ✅ | ❌ | 帮助提示 |
|
||||||
@ -112,6 +101,7 @@ function MyComponent() {
|
|||||||
| | CollapsibleSearchBar | ✅ | ❌ | 可折叠搜索栏 |
|
| | CollapsibleSearchBar | ✅ | ❌ | 可折叠搜索栏 |
|
||||||
| | ImageToolButton | ✅ | ❌ | 图片工具按钮 |
|
| | ImageToolButton | ✅ | ❌ | 图片工具按钮 |
|
||||||
| | DraggableList | ✅ | ❌ | 可拖拽列表 |
|
| | DraggableList | ✅ | ❌ | 可拖拽列表 |
|
||||||
|
| | CodeEditor | ✅ | ❌ | 代码编辑器 |
|
||||||
| | EmojiPicker | ❌ | ❌ | 表情选择器 (useTheme 依赖) |
|
| | EmojiPicker | ❌ | ❌ | 表情选择器 (useTheme 依赖) |
|
||||||
| | Selector | ✅ | ❌ | 选择器 (i18n 依赖) |
|
| | Selector | ✅ | ❌ | 选择器 (i18n 依赖) |
|
||||||
| | ModelSelector | ❌ | ❌ | 模型选择器 (Redux 依赖) |
|
| | ModelSelector | ❌ | ❌ | 模型选择器 (Redux 依赖) |
|
||||||
@ -122,7 +112,6 @@ function MyComponent() {
|
|||||||
| **未分类** | | | | 需要分类的组件 |
|
| **未分类** | | | | 需要分类的组件 |
|
||||||
| | Popups/* (16+ 文件) | ❌ | ❌ | 弹窗组件 (业务耦合) |
|
| | Popups/* (16+ 文件) | ❌ | ❌ | 弹窗组件 (业务耦合) |
|
||||||
| | RichEditor/* (30+ 文件) | ❌ | ❌ | 富文本编辑器 |
|
| | RichEditor/* (30+ 文件) | ❌ | ❌ | 富文本编辑器 |
|
||||||
| | CodeEditor/* | ❌ | ❌ | 代码编辑器 |
|
|
||||||
| | MarkdownEditor/* | ❌ | ❌ | Markdown 编辑器 |
|
| | MarkdownEditor/* | ❌ | ❌ | Markdown 编辑器 |
|
||||||
| | MinApp/* | ❌ | ❌ | 迷你应用 (Redux 依赖) |
|
| | MinApp/* | ❌ | ❌ | 迷你应用 (Redux 依赖) |
|
||||||
| | Avatar/* | ❌ | ❌ | 头像组件 |
|
| | Avatar/* | ❌ | ❌ | 头像组件 |
|
||||||
|
|||||||
@ -48,9 +48,9 @@ When submitting PRs, please place components in the correct directory based on t
|
|||||||
## Migration Overview
|
## Migration Overview
|
||||||
|
|
||||||
- **Total Components**: 236
|
- **Total Components**: 236
|
||||||
- **Migrated**: 43
|
- **Migrated**: 46
|
||||||
- **Refactored**: 0
|
- **Refactored**: 0
|
||||||
- **Pending Migration**: 193
|
- **Pending Migration**: 190
|
||||||
|
|
||||||
## Component Status Table
|
## Component Status Table
|
||||||
|
|
||||||
@ -61,6 +61,7 @@ When submitting PRs, please place components in the correct directory based on t
|
|||||||
| | CustomTag | ✅ | ❌ | Custom tag |
|
| | CustomTag | ✅ | ❌ | Custom tag |
|
||||||
| | DividerWithText | ✅ | ❌ | Divider with text |
|
| | DividerWithText | ✅ | ❌ | Divider with text |
|
||||||
| | EmojiIcon | ✅ | ❌ | Emoji icon |
|
| | EmojiIcon | ✅ | ❌ | Emoji icon |
|
||||||
|
| | ErrorBoundary | ✅ | ❌ | Error boundary (decoupled via props) |
|
||||||
| | ErrorTag | ✅ | ❌ | Error tag |
|
| | ErrorTag | ✅ | ❌ | Error tag |
|
||||||
| | IndicatorLight | ✅ | ❌ | Indicator light |
|
| | IndicatorLight | ✅ | ❌ | Indicator light |
|
||||||
| | Spinner | ✅ | ❌ | Loading spinner |
|
| | Spinner | ✅ | ❌ | Loading spinner |
|
||||||
@ -75,6 +76,7 @@ When submitting PRs, please place components in the correct directory based on t
|
|||||||
| | EmojiAvatar | ✅ | ❌ | Emoji avatar |
|
| | EmojiAvatar | ✅ | ❌ | Emoji avatar |
|
||||||
| | ListItem | ✅ | ❌ | List item |
|
| | ListItem | ✅ | ❌ | List item |
|
||||||
| | MaxContextCount | ✅ | ❌ | Max context count display |
|
| | MaxContextCount | ✅ | ❌ | Max context count display |
|
||||||
|
| | ProviderAvatar | ✅ | ❌ | Provider avatar |
|
||||||
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
|
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
|
||||||
| | OGCard | ❌ | ❌ | OG card |
|
| | OGCard | ❌ | ❌ | OG card |
|
||||||
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
|
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
|
||||||
@ -111,6 +113,7 @@ When submitting PRs, please place components in the correct directory based on t
|
|||||||
| | CollapsibleSearchBar | ✅ | ❌ | Collapsible search bar |
|
| | CollapsibleSearchBar | ✅ | ❌ | Collapsible search bar |
|
||||||
| | ImageToolButton | ✅ | ❌ | Image tool button |
|
| | ImageToolButton | ✅ | ❌ | Image tool button |
|
||||||
| | DraggableList | ✅ | ❌ | Draggable list |
|
| | DraggableList | ✅ | ❌ | Draggable list |
|
||||||
|
| | CodeEditor | ✅ | ❌ | Code editor |
|
||||||
| | EmojiPicker | ❌ | ❌ | Emoji picker (useTheme dependency) |
|
| | EmojiPicker | ❌ | ❌ | Emoji picker (useTheme dependency) |
|
||||||
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
|
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
|
||||||
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
|
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
|
||||||
@ -121,7 +124,6 @@ When submitting PRs, please place components in the correct directory based on t
|
|||||||
| **Uncategorized** | | | | Components needing categorization |
|
| **Uncategorized** | | | | Components needing categorization |
|
||||||
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
|
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
|
||||||
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
|
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
|
||||||
| | CodeEditor/* | ❌ | ❌ | Code editor |
|
|
||||||
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
|
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
|
||||||
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
|
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
|
||||||
| | Avatar/* | ❌ | ❌ | Avatar components |
|
| | Avatar/* | ❌ | ❌ | Avatar components |
|
||||||
|
|||||||
@ -15,8 +15,7 @@
|
|||||||
"lint": "eslint src --ext .ts,.tsx --fix",
|
"lint": "eslint src --ext .ts,.tsx --fix",
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build"
|
||||||
"update:languages": "tsx scripts/update-languages.ts"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ui",
|
"ui",
|
||||||
|
|||||||
@ -1,135 +0,0 @@
|
|||||||
import { exec } from 'child_process'
|
|
||||||
import * as fs from 'fs/promises'
|
|
||||||
import * as linguistLanguages from 'linguist-languages'
|
|
||||||
import * as path from 'path'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
|
|
||||||
const execAsync = promisify(exec)
|
|
||||||
|
|
||||||
type LanguageData = {
|
|
||||||
type: string
|
|
||||||
aliases?: string[]
|
|
||||||
extensions?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const LANGUAGES_FILE_PATH = path.join(__dirname, '../src/config/languages.ts')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts and filters necessary language data from the linguist-languages package.
|
|
||||||
* @returns A record of language data.
|
|
||||||
*/
|
|
||||||
function extractAllLanguageData(): Record<string, LanguageData> {
|
|
||||||
console.log('🔍 Extracting language data from linguist-languages...')
|
|
||||||
const languages = Object.entries(linguistLanguages).reduce(
|
|
||||||
(acc, [name, langData]) => {
|
|
||||||
const { type, extensions, aliases } = langData as any
|
|
||||||
|
|
||||||
// Only include languages with extensions or aliases
|
|
||||||
if ((extensions && extensions.length > 0) || (aliases && aliases.length > 0)) {
|
|
||||||
acc[name] = {
|
|
||||||
type: type || 'programming',
|
|
||||||
...(extensions && { extensions }),
|
|
||||||
...(aliases && { aliases })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, LanguageData>
|
|
||||||
)
|
|
||||||
console.log(`✅ Extracted ${Object.keys(languages).length} languages.`)
|
|
||||||
return languages
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates the content for the languages.ts file.
|
|
||||||
* @param languages The language data to include in the file.
|
|
||||||
* @returns The generated file content as a string.
|
|
||||||
*/
|
|
||||||
function generateLanguagesFileContent(languages: Record<string, LanguageData>): string {
|
|
||||||
console.log('📝 Generating languages.ts file content...')
|
|
||||||
const sortedLanguages = Object.fromEntries(Object.entries(languages).sort(([a], [b]) => a.localeCompare(b)))
|
|
||||||
|
|
||||||
const languagesObjectString = JSON.stringify(sortedLanguages, null, 2)
|
|
||||||
|
|
||||||
const content = `/**
|
|
||||||
* Code language list.
|
|
||||||
* Data source: linguist-languages
|
|
||||||
*
|
|
||||||
* ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
|
||||||
* THIS FILE IS AUTOMATICALLY GENERATED BY A SCRIPT. DO NOT EDIT IT MANUALLY!
|
|
||||||
* Run \`yarn update:languages\` to update this file.
|
|
||||||
* ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
type LanguageData = {
|
|
||||||
type: string;
|
|
||||||
aliases?: string[];
|
|
||||||
extensions?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const languages: Record<string, LanguageData> = ${languagesObjectString};
|
|
||||||
`
|
|
||||||
console.log('✅ File content generated.')
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a file using Prettier.
|
|
||||||
* @param filePath The path to the file to format.
|
|
||||||
*/
|
|
||||||
async function formatWithPrettier(filePath: string): Promise<void> {
|
|
||||||
console.log('🎨 Formatting file with Prettier...')
|
|
||||||
try {
|
|
||||||
await execAsync(`yarn prettier --write ${filePath}`)
|
|
||||||
console.log('✅ Prettier formatting complete.')
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error('❌ Prettier formatting failed:', e.stdout || e.stderr)
|
|
||||||
throw new Error('Prettier formatting failed.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks a file with TypeScript compiler.
|
|
||||||
* @param filePath The path to the file to check.
|
|
||||||
*/
|
|
||||||
async function checkTypeScript(filePath: string): Promise<void> {
|
|
||||||
console.log('🧐 Checking file with TypeScript compiler...')
|
|
||||||
try {
|
|
||||||
await execAsync(`yarn tsc --noEmit --skipLibCheck ${filePath}`)
|
|
||||||
console.log('✅ TypeScript check passed.')
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error('❌ TypeScript check failed:', e.stdout || e.stderr)
|
|
||||||
throw new Error('TypeScript check failed.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main function to update the languages.ts file.
|
|
||||||
*/
|
|
||||||
async function updateLanguagesFile(): Promise<void> {
|
|
||||||
console.log('🚀 Starting to update languages.ts...')
|
|
||||||
try {
|
|
||||||
const extractedLanguages = extractAllLanguageData()
|
|
||||||
const fileContent = generateLanguagesFileContent(extractedLanguages)
|
|
||||||
|
|
||||||
await fs.writeFile(LANGUAGES_FILE_PATH, fileContent, 'utf-8')
|
|
||||||
console.log(`✅ Successfully wrote to ${LANGUAGES_FILE_PATH}`)
|
|
||||||
|
|
||||||
await formatWithPrettier(LANGUAGES_FILE_PATH)
|
|
||||||
await checkTypeScript(LANGUAGES_FILE_PATH)
|
|
||||||
|
|
||||||
console.log('🎉 Successfully updated languages.ts file!')
|
|
||||||
console.log(`📊 Contains ${Object.keys(extractedLanguages).length} languages.`)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ An error occurred during the update process:', (error as Error).message)
|
|
||||||
// No need to restore backup as we write only at the end of successful generation.
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
updateLanguagesFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
export { updateLanguagesFile }
|
|
||||||
101
packages/ui/src/components/base/ErrorBoundary/index.tsx
Normal file
101
packages/ui/src/components/base/ErrorBoundary/index.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// Original path: src/renderer/src/components/ErrorBoundary.tsx
|
||||||
|
import { Button } from '@heroui/button'
|
||||||
|
import { Alert, Space } from 'antd'
|
||||||
|
import { ComponentType, ReactNode } from 'react'
|
||||||
|
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { formatErrorMessage } from './utils'
|
||||||
|
|
||||||
|
interface CustomFallbackProps extends FallbackProps {
|
||||||
|
onDebugClick?: () => void | Promise<void>
|
||||||
|
onReloadClick?: () => void | Promise<void>
|
||||||
|
debugButtonText?: string
|
||||||
|
reloadButtonText?: string
|
||||||
|
errorMessage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultFallback: ComponentType<CustomFallbackProps> = (props: CustomFallbackProps): ReactNode => {
|
||||||
|
const {
|
||||||
|
error,
|
||||||
|
onDebugClick,
|
||||||
|
onReloadClick,
|
||||||
|
debugButtonText = 'Open DevTools',
|
||||||
|
reloadButtonText = 'Reload',
|
||||||
|
errorMessage = 'An error occurred'
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorContainer>
|
||||||
|
<Alert
|
||||||
|
message={errorMessage}
|
||||||
|
showIcon
|
||||||
|
description={formatErrorMessage(error)}
|
||||||
|
type="error"
|
||||||
|
action={
|
||||||
|
<Space>
|
||||||
|
{onDebugClick && (
|
||||||
|
<Button size="sm" onPress={onDebugClick}>
|
||||||
|
{debugButtonText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onReloadClick && (
|
||||||
|
<Button size="sm" onPress={onReloadClick}>
|
||||||
|
{reloadButtonText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ErrorContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorBoundaryCustomizedProps {
|
||||||
|
children: ReactNode
|
||||||
|
fallbackComponent?: ComponentType<CustomFallbackProps>
|
||||||
|
onDebugClick?: () => void | Promise<void>
|
||||||
|
onReloadClick?: () => void | Promise<void>
|
||||||
|
debugButtonText?: string
|
||||||
|
reloadButtonText?: string
|
||||||
|
errorMessage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorBoundaryCustomized = ({
|
||||||
|
children,
|
||||||
|
fallbackComponent,
|
||||||
|
onDebugClick,
|
||||||
|
onReloadClick,
|
||||||
|
debugButtonText,
|
||||||
|
reloadButtonText,
|
||||||
|
errorMessage
|
||||||
|
}: ErrorBoundaryCustomizedProps) => {
|
||||||
|
const FallbackComponent = fallbackComponent ?? DefaultFallback
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary
|
||||||
|
FallbackComponent={(props: FallbackProps) => (
|
||||||
|
<FallbackComponent
|
||||||
|
{...props}
|
||||||
|
onDebugClick={onDebugClick}
|
||||||
|
onReloadClick={onReloadClick}
|
||||||
|
debugButtonText={debugButtonText}
|
||||||
|
reloadButtonText={reloadButtonText}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export { ErrorBoundaryCustomized as ErrorBoundary }
|
||||||
|
export type { ErrorBoundaryCustomizedProps, CustomFallbackProps }
|
||||||
8
packages/ui/src/components/base/ErrorBoundary/utils.ts
Normal file
8
packages/ui/src/components/base/ErrorBoundary/utils.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Utility functions for ErrorBoundary component
|
||||||
|
|
||||||
|
export function formatErrorMessage(error: Error): string {
|
||||||
|
if (error.message) {
|
||||||
|
return error.message
|
||||||
|
}
|
||||||
|
return error.toString()
|
||||||
|
}
|
||||||
@ -1,17 +0,0 @@
|
|||||||
// Original path: src/renderer/src/components/Tags/ErrorTag.tsx
|
|
||||||
import { CircleXIcon } from 'lucide-react'
|
|
||||||
|
|
||||||
import CustomTag from '../CustomTag'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
iconSize?: number
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ErrorTag = ({ iconSize: size = 14, message }: Props) => {
|
|
||||||
return (
|
|
||||||
<CustomTag icon={<CircleXIcon size={size} color="var(--color-status-error)" />} color="var(--color-status-error)">
|
|
||||||
{message}
|
|
||||||
</CustomTag>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
51
packages/ui/src/components/base/StatusTag/index.tsx
Normal file
51
packages/ui/src/components/base/StatusTag/index.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { AlertTriangleIcon, CheckIcon, CircleXIcon, InfoIcon, LucideIcon } 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }) => {
|
||||||
|
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}>
|
||||||
|
{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} />
|
||||||
|
)
|
||||||
@ -1,17 +0,0 @@
|
|||||||
// Original path: src/renderer/src/components/Tags/SuccessTag.tsx
|
|
||||||
import { CheckIcon } from 'lucide-react'
|
|
||||||
|
|
||||||
import CustomTag from '../CustomTag'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
iconSize?: number
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SuccessTag = ({ iconSize: size = 14, message }: Props) => {
|
|
||||||
return (
|
|
||||||
<CustomTag icon={<CheckIcon size={size} color="var(--color-status-success)" />} color="var(--color-status-success)">
|
|
||||||
{message}
|
|
||||||
</CustomTag>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
// Original path: src/renderer/src/components/Tags/WarnTag.tsx
|
|
||||||
import { AlertTriangleIcon } from 'lucide-react'
|
|
||||||
|
|
||||||
import CustomTag from '../CustomTag'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
iconSize?: number
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WarnTag = ({ iconSize: size = 14, message }: Props) => {
|
|
||||||
return (
|
|
||||||
<CustomTag
|
|
||||||
icon={<AlertTriangleIcon size={size} color="var(--color-status-warning)" />}
|
|
||||||
color="var(--color-status-warning)">
|
|
||||||
{message}
|
|
||||||
</CustomTag>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
86
packages/ui/src/components/display/ProviderAvatar/index.tsx
Normal file
86
packages/ui/src/components/display/ProviderAvatar/index.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// Original path: src/renderer/src/components/ProviderAvatar.tsx
|
||||||
|
import { Avatar } from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { generateColorFromChar, getFirstCharacter, getForegroundColor } from './utils'
|
||||||
|
|
||||||
|
interface ProviderAvatarProps {
|
||||||
|
providerId: string
|
||||||
|
providerName: string
|
||||||
|
logoSrc?: string
|
||||||
|
size?: number
|
||||||
|
className?: string
|
||||||
|
style?: React.CSSProperties
|
||||||
|
renderCustomLogo?: (providerId: string) => React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProviderLogo = styled(Avatar)`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
`
|
||||||
|
|
||||||
|
const ProviderSvgLogo = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
border-radius: 100%;
|
||||||
|
|
||||||
|
& > svg {
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ProviderAvatar: React.FC<ProviderAvatarProps> = ({
|
||||||
|
providerId,
|
||||||
|
providerName,
|
||||||
|
logoSrc,
|
||||||
|
size,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
renderCustomLogo
|
||||||
|
}) => {
|
||||||
|
// Check if custom logo renderer is provided for special providers
|
||||||
|
if (renderCustomLogo) {
|
||||||
|
const customLogo = renderCustomLogo(providerId)
|
||||||
|
if (customLogo) {
|
||||||
|
return (
|
||||||
|
<ProviderSvgLogo className={className} style={style}>
|
||||||
|
{customLogo}
|
||||||
|
</ProviderSvgLogo>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If logo source is provided, render image avatar
|
||||||
|
if (logoSrc) {
|
||||||
|
return (
|
||||||
|
<ProviderLogo draggable="false" shape="circle" src={logoSrc} className={className} style={style} size={size} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: generate avatar with first character and background color
|
||||||
|
const backgroundColor = generateColorFromChar(providerName)
|
||||||
|
const color = providerName ? getForegroundColor(backgroundColor) : 'white'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProviderLogo
|
||||||
|
size={size}
|
||||||
|
shape="circle"
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
backgroundColor,
|
||||||
|
color,
|
||||||
|
...style
|
||||||
|
}}>
|
||||||
|
{getFirstCharacter(providerName)}
|
||||||
|
</ProviderLogo>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProviderAvatar
|
||||||
37
packages/ui/src/components/display/ProviderAvatar/utils.ts
Normal file
37
packages/ui/src/components/display/ProviderAvatar/utils.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Utility functions for ProviderAvatar component
|
||||||
|
|
||||||
|
export function generateColorFromChar(char: string): string {
|
||||||
|
const seed = char.charCodeAt(0)
|
||||||
|
const a = 1664525
|
||||||
|
const c = 1013904223
|
||||||
|
const m = Math.pow(2, 32)
|
||||||
|
|
||||||
|
let r = (a * seed + c) % m
|
||||||
|
let g = (a * r + c) % m
|
||||||
|
let b = (a * g + c) % m
|
||||||
|
|
||||||
|
r = Math.floor((r / m) * 256)
|
||||||
|
g = Math.floor((g / m) * 256)
|
||||||
|
b = Math.floor((b / m) * 256)
|
||||||
|
|
||||||
|
const toHex = (n: number) => n.toString(16).padStart(2, '0')
|
||||||
|
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFirstCharacter(str: string): string {
|
||||||
|
for (const char of str) {
|
||||||
|
return char
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getForegroundColor(backgroundColor: string): string {
|
||||||
|
// Simple luminance calculation
|
||||||
|
const hex = backgroundColor.replace('#', '')
|
||||||
|
const r = parseInt(hex.substring(0, 2), 16) / 255
|
||||||
|
const g = parseInt(hex.substring(2, 4), 16) / 255
|
||||||
|
const b = parseInt(hex.substring(4, 6), 16) / 255
|
||||||
|
|
||||||
|
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||||
|
return luminance > 0.179 ? '#000000' : '#FFFFFF'
|
||||||
|
}
|
||||||
@ -1,6 +0,0 @@
|
|||||||
// Original path: src/renderer/src/components/Icons/CopyIcon.tsx
|
|
||||||
import { Copy } from 'lucide-react'
|
|
||||||
|
|
||||||
const CopyIcon = (props: React.ComponentProps<typeof Copy>) => <Copy size="1rem" {...props} />
|
|
||||||
|
|
||||||
export default CopyIcon
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
// Original path: src/renderer/src/components/Icons/DeleteIcon.tsx
|
|
||||||
import { Trash } from 'lucide-react'
|
|
||||||
|
|
||||||
const DeleteIcon = (props: React.ComponentProps<typeof Trash>) => <Trash size="1rem" {...props} />
|
|
||||||
|
|
||||||
export default DeleteIcon
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
// Original path: src/renderer/src/components/Icons/EditIcon.tsx
|
|
||||||
import { Pencil } from 'lucide-react'
|
|
||||||
|
|
||||||
const EditIcon = (props: React.ComponentProps<typeof Pencil>) => <Pencil size="1rem" {...props} />
|
|
||||||
|
|
||||||
export default EditIcon
|
|
||||||
41
packages/ui/src/components/icons/Icon/index.tsx
Normal file
41
packages/ui/src/components/icons/Icon/index.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
AlignLeft,
|
||||||
|
Copy,
|
||||||
|
Eye,
|
||||||
|
LucideIcon,
|
||||||
|
Pencil,
|
||||||
|
RefreshCw,
|
||||||
|
RotateCcw,
|
||||||
|
ScanLine,
|
||||||
|
Search,
|
||||||
|
Trash,
|
||||||
|
WrapText,
|
||||||
|
Wrench
|
||||||
|
} from 'lucide-react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
// 创建一个 Icon 工厂函数
|
||||||
|
export function createIcon(IconComponent: LucideIcon, defaultSize: string | number = '1rem') {
|
||||||
|
const Icon = React.forwardRef<SVGSVGElement, React.ComponentProps<typeof IconComponent>>(
|
||||||
|
(props, ref) => <IconComponent ref={ref} size={defaultSize} {...props} />
|
||||||
|
)
|
||||||
|
Icon.displayName = `Icon(${IconComponent.displayName || IconComponent.name})`
|
||||||
|
return Icon
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预定义的常用图标(向后兼容,只导入需要的图标)
|
||||||
|
export const CopyIcon = createIcon(Copy)
|
||||||
|
export const DeleteIcon = createIcon(Trash)
|
||||||
|
export const EditIcon = createIcon(Pencil)
|
||||||
|
export const RefreshIcon = createIcon(RefreshCw)
|
||||||
|
export const ResetIcon = createIcon(RotateCcw)
|
||||||
|
export const ToolIcon = createIcon(Wrench)
|
||||||
|
export const VisionIcon = createIcon(Eye)
|
||||||
|
export const WebSearchIcon = createIcon(Search)
|
||||||
|
export const WrapIcon = createIcon(WrapText)
|
||||||
|
export const UnWrapIcon = createIcon(AlignLeft)
|
||||||
|
export const OcrIcon = createIcon(ScanLine)
|
||||||
|
|
||||||
|
// 导出 createIcon 以便用户自行创建图标组件
|
||||||
|
export type { LucideIcon }
|
||||||
|
export type { LucideProps } from 'lucide-react'
|
||||||
@ -1,8 +0,0 @@
|
|||||||
// Original path: src/renderer/src/components/Icons/OcrIcon.tsx
|
|
||||||
import { FC } from 'react'
|
|
||||||
|
|
||||||
const OcrIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
|
||||||
return <i {...props} className={`iconfont icon-OCRshibie ${props.className}`} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OcrIcon
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
// Original path: src/renderer/src/components/Icons/RefreshIcon.tsx
|
|
||||||
import { RefreshCw } from 'lucide-react'
|
|
||||||
|
|
||||||
const RefreshIcon = (props: React.ComponentProps<typeof RefreshCw>) => <RefreshCw size="1rem" {...props} />
|
|
||||||
|
|
||||||
export default RefreshIcon
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
// Original path: src/renderer/src/components/Icons/ResetIcon.tsx
|
|
||||||
import { RotateCcw } from 'lucide-react'
|
|
||||||
|
|
||||||
const ResetIcon = (props: React.ComponentProps<typeof RotateCcw>) => <RotateCcw size="1rem" {...props} />
|
|
||||||
|
|
||||||
export default ResetIcon
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
// Original path: src/renderer/src/components/Icons/ToolIcon.tsx
|
|
||||||
import { FC } from 'react'
|
|
||||||
|
|
||||||
const ToolIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
|
||||||
return <i {...props} className={`iconfont icon-plugin ${props.className}`} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ToolIcon
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
// Original path: src/renderer/src/components/Icons/UnWrapIcon.tsx
|
|
||||||
const UnWrapIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="1em"
|
|
||||||
height="1em"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
className="unwrap_svg__lucide unwrap_svg__lucide-text unwrap_svg__size-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
{...props}>
|
|
||||||
<path d="M17 6.1H3M21 12.1H3M15.1 18H3" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
export default UnWrapIcon
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
// Original: src/renderer/src/components/Icons/VisionIcon.tsx
|
|
||||||
import { Tooltip } from 'antd'
|
|
||||||
import { ImageIcon } from 'lucide-react'
|
|
||||||
import React, { FC } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
const VisionIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Tooltip title={t('models.type.vision')} placement="top">
|
|
||||||
<Icon size={15} {...(props as any)} />
|
|
||||||
</Tooltip>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Icon = styled(ImageIcon)`
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-right: 6px;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default VisionIcon
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
// Original: src/renderer/src/components/Icons/WebSearchIcon.tsx
|
|
||||||
import { GlobalOutlined } from '@ant-design/icons'
|
|
||||||
import { Tooltip } from 'antd'
|
|
||||||
import React, { FC } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
const WebSearchIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Tooltip title={t('models.type.websearch')} placement="top">
|
|
||||||
<Icon {...(props as any)} />
|
|
||||||
</Tooltip>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Icon = styled(GlobalOutlined)`
|
|
||||||
color: var(--color-link);
|
|
||||||
font-size: 15px;
|
|
||||||
margin-right: 6px;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default WebSearchIcon
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
// Original path: src/renderer/src/components/Icons/WrapIcon.tsx
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
const WrapIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="1em"
|
|
||||||
height="1em"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
className="wrap_svg__lucide wrap_svg__lucide-wrap-text wrap_svg__size-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
{...props}>
|
|
||||||
<path d="M3 6h18M3 12h15a3 3 0 1 1 0 6h-4" />
|
|
||||||
<path d="m16 16-2 2 2 2M3 18h7" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
export default WrapIcon
|
|
||||||
@ -4,12 +4,13 @@ export { default as CustomCollapse } from './base/CustomCollapse'
|
|||||||
export { default as CustomTag } from './base/CustomTag'
|
export { default as CustomTag } from './base/CustomTag'
|
||||||
export { default as DividerWithText } from './base/DividerWithText'
|
export { default as DividerWithText } from './base/DividerWithText'
|
||||||
export { default as EmojiIcon } from './base/EmojiIcon'
|
export { default as EmojiIcon } from './base/EmojiIcon'
|
||||||
export { ErrorTag } from './base/ErrorTag'
|
export { ErrorBoundary } from './base/ErrorBoundary'
|
||||||
|
export type { ErrorBoundaryCustomizedProps, CustomFallbackProps } from './base/ErrorBoundary'
|
||||||
export { default as IndicatorLight } from './base/IndicatorLight'
|
export { default as IndicatorLight } from './base/IndicatorLight'
|
||||||
export { default as Spinner } from './base/Spinner'
|
export { default as Spinner } from './base/Spinner'
|
||||||
export { SuccessTag } from './base/SuccessTag'
|
export { StatusTag, ErrorTag, SuccessTag, WarnTag, InfoTag } from './base/StatusTag'
|
||||||
|
export type { StatusType, StatusTagProps } from './base/StatusTag'
|
||||||
export { default as TextBadge } from './base/TextBadge'
|
export { default as TextBadge } from './base/TextBadge'
|
||||||
export { WarnTag } from './base/WarnTag'
|
|
||||||
|
|
||||||
// Display Components
|
// Display Components
|
||||||
export { default as Ellipsis } from './display/Ellipsis'
|
export { default as Ellipsis } from './display/Ellipsis'
|
||||||
@ -17,6 +18,7 @@ export { default as EmojiAvatar } from './display/EmojiAvatar'
|
|||||||
export { default as ExpandableText } from './display/ExpandableText'
|
export { default as ExpandableText } from './display/ExpandableText'
|
||||||
export { default as ListItem } from './display/ListItem'
|
export { default as ListItem } from './display/ListItem'
|
||||||
export { default as MaxContextCount } from './display/MaxContextCount'
|
export { default as MaxContextCount } from './display/MaxContextCount'
|
||||||
|
export { ProviderAvatar } from './display/ProviderAvatar'
|
||||||
export { default as ThinkingEffect } from './display/ThinkingEffect'
|
export { default as ThinkingEffect } from './display/ThinkingEffect'
|
||||||
|
|
||||||
// Layout Components
|
// Layout Components
|
||||||
@ -24,21 +26,25 @@ export { default as HorizontalScrollContainer } from './layout/HorizontalScrollC
|
|||||||
export { default as Scrollbar } from './layout/Scrollbar'
|
export { default as Scrollbar } from './layout/Scrollbar'
|
||||||
|
|
||||||
// Icon Components
|
// Icon Components
|
||||||
export { default as CopyIcon } from './icons/CopyIcon'
|
export {
|
||||||
export { default as DeleteIcon } from './icons/DeleteIcon'
|
createIcon,
|
||||||
export { default as EditIcon } from './icons/EditIcon'
|
CopyIcon,
|
||||||
|
DeleteIcon,
|
||||||
|
EditIcon,
|
||||||
|
RefreshIcon,
|
||||||
|
ResetIcon,
|
||||||
|
ToolIcon,
|
||||||
|
VisionIcon,
|
||||||
|
WebSearchIcon,
|
||||||
|
WrapIcon,
|
||||||
|
UnWrapIcon,
|
||||||
|
OcrIcon
|
||||||
|
} from './icons/Icon'
|
||||||
|
export type { LucideIcon, LucideProps } from './icons/Icon'
|
||||||
export { FilePngIcon, FileSvgIcon } from './icons/FileIcons'
|
export { FilePngIcon, FileSvgIcon } from './icons/FileIcons'
|
||||||
export { default as OcrIcon } from './icons/OcrIcon'
|
|
||||||
export { default as ReasoningIcon } from './icons/ReasoningIcon'
|
export { default as ReasoningIcon } from './icons/ReasoningIcon'
|
||||||
export { default as RefreshIcon } from './icons/RefreshIcon'
|
|
||||||
export { default as ResetIcon } from './icons/ResetIcon'
|
|
||||||
export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring'
|
export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring'
|
||||||
export { default as ToolIcon } from './icons/ToolIcon'
|
|
||||||
export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon'
|
export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon'
|
||||||
export { default as UnWrapIcon } from './icons/UnWrapIcon'
|
|
||||||
export { default as VisionIcon } from './icons/VisionIcon'
|
|
||||||
export { default as WebSearchIcon } from './icons/WebSearchIcon'
|
|
||||||
export { default as WrapIcon } from './icons/WrapIcon'
|
|
||||||
|
|
||||||
// Interactive Components
|
// Interactive Components
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ const CodeEditor = ({
|
|||||||
value,
|
value,
|
||||||
placeholder,
|
placeholder,
|
||||||
language,
|
language,
|
||||||
|
languageConfig,
|
||||||
onSave,
|
onSave,
|
||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
@ -55,7 +56,7 @@ const CodeEditor = ({
|
|||||||
const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? ''))
|
const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? ''))
|
||||||
const editorViewRef = useRef<EditorView | null>(null)
|
const editorViewRef = useRef<EditorView | null>(null)
|
||||||
|
|
||||||
const langExtensions = useLanguageExtensions(language, options?.lint)
|
const langExtensions = useLanguageExtensions(language, options?.lint, languageConfig)
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
const currentDoc = editorViewRef.current?.state.doc.toString() ?? ''
|
const currentDoc = editorViewRef.current?.state.doc.toString() ?? ''
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { EditorView } from '@codemirror/view'
|
|||||||
import { Extension, keymap } from '@uiw/react-codemirror'
|
import { Extension, keymap } from '@uiw/react-codemirror'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
import { LanguageConfig } from './types'
|
||||||
import { getNormalizedExtension } from './utils'
|
import { getNormalizedExtension } from './utils'
|
||||||
|
|
||||||
/** 语言对应的 linter 加载器
|
/** 语言对应的 linter 加载器
|
||||||
@ -34,8 +35,8 @@ const specialLanguageLoaders: Record<string, () => Promise<Extension>> = {
|
|||||||
/**
|
/**
|
||||||
* 加载语言扩展
|
* 加载语言扩展
|
||||||
*/
|
*/
|
||||||
async function loadLanguageExtension(language: string): Promise<Extension | null> {
|
async function loadLanguageExtension(language: string, languageConfig?: LanguageConfig): Promise<Extension | null> {
|
||||||
const fileExt = await getNormalizedExtension(language)
|
const fileExt = await getNormalizedExtension(language, languageConfig)
|
||||||
|
|
||||||
// 尝试加载特殊语言
|
// 尝试加载特殊语言
|
||||||
const specialLoader = specialLanguageLoaders[fileExt]
|
const specialLoader = specialLanguageLoaders[fileExt]
|
||||||
@ -62,8 +63,8 @@ async function loadLanguageExtension(language: string): Promise<Extension | null
|
|||||||
/**
|
/**
|
||||||
* 加载 linter 扩展
|
* 加载 linter 扩展
|
||||||
*/
|
*/
|
||||||
async function loadLinterExtension(language: string): Promise<Extension | null> {
|
async function loadLinterExtension(language: string, languageConfig?: LanguageConfig): Promise<Extension | null> {
|
||||||
const fileExt = await getNormalizedExtension(language)
|
const fileExt = await getNormalizedExtension(language, languageConfig)
|
||||||
|
|
||||||
const loader = linterLoaders[fileExt]
|
const loader = linterLoaders[fileExt]
|
||||||
if (!loader) return null
|
if (!loader) return null
|
||||||
@ -79,7 +80,7 @@ async function loadLinterExtension(language: string): Promise<Extension | null>
|
|||||||
/**
|
/**
|
||||||
* 加载语言相关扩展
|
* 加载语言相关扩展
|
||||||
*/
|
*/
|
||||||
export const useLanguageExtensions = (language: string, lint?: boolean) => {
|
export const useLanguageExtensions = (language: string, lint?: boolean, languageConfig?: LanguageConfig) => {
|
||||||
const [extensions, setExtensions] = useState<Extension[]>([])
|
const [extensions, setExtensions] = useState<Extension[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -89,8 +90,8 @@ export const useLanguageExtensions = (language: string, lint?: boolean) => {
|
|||||||
try {
|
try {
|
||||||
// 加载所有扩展
|
// 加载所有扩展
|
||||||
const [languageResult, linterResult] = await Promise.allSettled([
|
const [languageResult, linterResult] = await Promise.allSettled([
|
||||||
loadLanguageExtension(language),
|
loadLanguageExtension(language, languageConfig),
|
||||||
lint ? loadLinterExtension(language) : Promise.resolve(null)
|
lint ? loadLinterExtension(language, languageConfig) : Promise.resolve(null)
|
||||||
])
|
])
|
||||||
|
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
@ -121,7 +122,7 @@ export const useLanguageExtensions = (language: string, lint?: boolean) => {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [language, lint])
|
}, [language, lint, languageConfig])
|
||||||
|
|
||||||
return extensions
|
return extensions
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,16 @@ import { BasicSetupOptions, Extension } from '@uiw/react-codemirror'
|
|||||||
|
|
||||||
export type CodeMirrorTheme = 'light' | 'dark' | 'none' | Extension
|
export type CodeMirrorTheme = 'light' | 'dark' | 'none' | Extension
|
||||||
|
|
||||||
|
/** Language data structure for file extension mapping */
|
||||||
|
export interface LanguageData {
|
||||||
|
type: string
|
||||||
|
aliases?: string[]
|
||||||
|
extensions?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Language configuration mapping language names to their data */
|
||||||
|
export type LanguageConfig = Record<string, LanguageData>
|
||||||
|
|
||||||
export interface CodeEditorHandles {
|
export interface CodeEditorHandles {
|
||||||
save?: () => void
|
save?: () => void
|
||||||
}
|
}
|
||||||
@ -20,6 +30,12 @@ export interface CodeEditorProps {
|
|||||||
* - Supports file extensions: .cpp/cpp, .js/js, .py/py, etc.
|
* - Supports file extensions: .cpp/cpp, .js/js, .py/py, etc.
|
||||||
*/
|
*/
|
||||||
language: string
|
language: string
|
||||||
|
/**
|
||||||
|
* Language configuration for extension mapping.
|
||||||
|
* If not provided, will use a default minimal configuration.
|
||||||
|
* @optional
|
||||||
|
*/
|
||||||
|
languageConfig?: LanguageConfig
|
||||||
/** Fired when ref.save() is called or the save shortcut is triggered. */
|
/** Fired when ref.save() is called or the save shortcut is triggered. */
|
||||||
onSave?: (newContent: string) => void
|
onSave?: (newContent: string) => void
|
||||||
/** Fired when the editor content changes. */
|
/** Fired when the editor content changes. */
|
||||||
|
|||||||
@ -2,8 +2,7 @@ import * as cmThemes from '@uiw/codemirror-themes-all'
|
|||||||
import { Extension } from '@uiw/react-codemirror'
|
import { Extension } from '@uiw/react-codemirror'
|
||||||
import diff from 'fast-diff'
|
import diff from 'fast-diff'
|
||||||
|
|
||||||
import { getExtensionByLanguage } from '../../../utils/codeLanguage'
|
import { CodeMirrorTheme, LanguageConfig } from './types'
|
||||||
import { CodeMirrorTheme } from './types'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes code changes using fast-diff and converts them to CodeMirror changes.
|
* Computes code changes using fast-diff and converts them to CodeMirror changes.
|
||||||
@ -50,15 +49,158 @@ const _customLanguageExtensions: Record<string, string> = {
|
|||||||
graphviz: 'dot'
|
graphviz: 'dot'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default minimal language configuration for common languages
|
||||||
|
const _defaultLanguageConfig: LanguageConfig = {
|
||||||
|
JavaScript: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.js', '.mjs', '.cjs'],
|
||||||
|
aliases: ['js', 'node']
|
||||||
|
},
|
||||||
|
TypeScript: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.ts'],
|
||||||
|
aliases: ['ts']
|
||||||
|
},
|
||||||
|
Python: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.py'],
|
||||||
|
aliases: ['python3', 'py']
|
||||||
|
},
|
||||||
|
Java: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.java']
|
||||||
|
},
|
||||||
|
'C++': {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.cpp', '.cc', '.cxx'],
|
||||||
|
aliases: ['cpp']
|
||||||
|
},
|
||||||
|
C: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.c']
|
||||||
|
},
|
||||||
|
'C#': {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.cs'],
|
||||||
|
aliases: ['csharp']
|
||||||
|
},
|
||||||
|
HTML: {
|
||||||
|
type: 'markup',
|
||||||
|
extensions: ['.html', '.htm']
|
||||||
|
},
|
||||||
|
CSS: {
|
||||||
|
type: 'markup',
|
||||||
|
extensions: ['.css']
|
||||||
|
},
|
||||||
|
JSON: {
|
||||||
|
type: 'data',
|
||||||
|
extensions: ['.json']
|
||||||
|
},
|
||||||
|
XML: {
|
||||||
|
type: 'data',
|
||||||
|
extensions: ['.xml']
|
||||||
|
},
|
||||||
|
YAML: {
|
||||||
|
type: 'data',
|
||||||
|
extensions: ['.yml', '.yaml']
|
||||||
|
},
|
||||||
|
SQL: {
|
||||||
|
type: 'data',
|
||||||
|
extensions: ['.sql']
|
||||||
|
},
|
||||||
|
Shell: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.sh', '.bash'],
|
||||||
|
aliases: ['bash', 'sh']
|
||||||
|
},
|
||||||
|
Go: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.go'],
|
||||||
|
aliases: ['golang']
|
||||||
|
},
|
||||||
|
Rust: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.rs']
|
||||||
|
},
|
||||||
|
PHP: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.php']
|
||||||
|
},
|
||||||
|
Ruby: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.rb'],
|
||||||
|
aliases: ['rb']
|
||||||
|
},
|
||||||
|
Swift: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.swift']
|
||||||
|
},
|
||||||
|
Kotlin: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.kt']
|
||||||
|
},
|
||||||
|
Dart: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.dart']
|
||||||
|
},
|
||||||
|
R: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.r']
|
||||||
|
},
|
||||||
|
MATLAB: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.m']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file extension of the language, by language name
|
||||||
|
* - First, exact match
|
||||||
|
* - Then, case-insensitive match
|
||||||
|
* - Finally, match aliases
|
||||||
|
* If there are multiple file extensions, only the first one will be returned
|
||||||
|
* @param language language name
|
||||||
|
* @param languageConfig optional language configuration, defaults to a minimal config
|
||||||
|
* @returns file extension
|
||||||
|
*/
|
||||||
|
export function getExtensionByLanguage(language: string, languageConfig?: LanguageConfig): string {
|
||||||
|
const languages = languageConfig || _defaultLanguageConfig
|
||||||
|
const lowerLanguage = language.toLowerCase()
|
||||||
|
|
||||||
|
// Exact match language name
|
||||||
|
const directMatch = languages[language]
|
||||||
|
if (directMatch?.extensions?.[0]) {
|
||||||
|
return directMatch.extensions[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case-insensitive match language name
|
||||||
|
for (const [langName, data] of Object.entries(languages)) {
|
||||||
|
if (langName.toLowerCase() === lowerLanguage && data.extensions?.[0]) {
|
||||||
|
return data.extensions[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match aliases
|
||||||
|
for (const [, data] of Object.entries(languages)) {
|
||||||
|
if (data.aliases?.some((alias) => alias.toLowerCase() === lowerLanguage)) {
|
||||||
|
return data.extensions?.[0] || `.${language}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to language name
|
||||||
|
return `.${language}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the file extension of the language, for @uiw/codemirror-extensions-langs
|
* Get the file extension of the language, for @uiw/codemirror-extensions-langs
|
||||||
* - First, search for custom extensions
|
* - First, search for custom extensions
|
||||||
* - Then, search for github linguist extensions
|
* - Then, search for language configuration extensions
|
||||||
* - Finally, assume the name is already an extension
|
* - Finally, assume the name is already an extension
|
||||||
* @param language language name
|
* @param language language name
|
||||||
|
* @param languageConfig optional language configuration
|
||||||
* @returns file extension (without `.` prefix)
|
* @returns file extension (without `.` prefix)
|
||||||
*/
|
*/
|
||||||
export async function getNormalizedExtension(language: string) {
|
export async function getNormalizedExtension(language: string, languageConfig?: LanguageConfig) {
|
||||||
let lang = language
|
let lang = language
|
||||||
|
|
||||||
// If the language name looks like an extension, remove the dot
|
// If the language name looks like an extension, remove the dot
|
||||||
@ -74,8 +216,8 @@ export async function getNormalizedExtension(language: string) {
|
|||||||
return customExt
|
return customExt
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Search for github linguist extensions
|
// 2. Search for language configuration extensions
|
||||||
const linguistExt = getExtensionByLanguage(lang)
|
const linguistExt = getExtensionByLanguage(lang, languageConfig)
|
||||||
if (linguistExt) {
|
if (linguistExt) {
|
||||||
return linguistExt.slice(1)
|
return linguistExt.slice(1)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,39 +0,0 @@
|
|||||||
import { languages } from '../config/languages'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the file extension of the language, by language name
|
|
||||||
* - First, exact match
|
|
||||||
* - Then, case-insensitive match
|
|
||||||
* - Finally, match aliases
|
|
||||||
* If there are multiple file extensions, only the first one will be returned
|
|
||||||
* @param language language name
|
|
||||||
* @returns file extension
|
|
||||||
*/
|
|
||||||
export function getExtensionByLanguage(language: string): string {
|
|
||||||
const lowerLanguage = language.toLowerCase()
|
|
||||||
|
|
||||||
// Exact match language name
|
|
||||||
const directMatch = languages[language]
|
|
||||||
if (directMatch?.extensions?.[0]) {
|
|
||||||
return directMatch.extensions[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
const languageEntries = Object.entries(languages)
|
|
||||||
|
|
||||||
// Case-insensitive match language name
|
|
||||||
for (const [langName, data] of languageEntries) {
|
|
||||||
if (langName.toLowerCase() === lowerLanguage && data.extensions?.[0]) {
|
|
||||||
return data.extensions[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match aliases
|
|
||||||
for (const [, data] of languageEntries) {
|
|
||||||
if (data.aliases?.some((alias) => alias.toLowerCase() === lowerLanguage)) {
|
|
||||||
return data.extensions?.[0] || `.${language}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to language name
|
|
||||||
return `.${language}`
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
176
packages/ui/stories/components/base/StatusTag.stories.tsx
Normal file
176
packages/ui/stories/components/base/StatusTag.stories.tsx
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,110 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
290
packages/ui/stories/components/icons/Icon.stories.tsx
Normal file
290
packages/ui/stories/components/icons/Icon.stories.tsx
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import {
|
||||||
|
Copy,
|
||||||
|
Trash,
|
||||||
|
Pencil,
|
||||||
|
RefreshCw,
|
||||||
|
RotateCcw,
|
||||||
|
Wrench,
|
||||||
|
Eye,
|
||||||
|
Search,
|
||||||
|
WrapText,
|
||||||
|
AlignLeft,
|
||||||
|
ScanLine,
|
||||||
|
Settings,
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
ChevronRight
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createIcon,
|
||||||
|
CopyIcon,
|
||||||
|
DeleteIcon,
|
||||||
|
EditIcon,
|
||||||
|
RefreshIcon,
|
||||||
|
ResetIcon,
|
||||||
|
ToolIcon,
|
||||||
|
VisionIcon,
|
||||||
|
WebSearchIcon,
|
||||||
|
WrapIcon,
|
||||||
|
UnWrapIcon,
|
||||||
|
OcrIcon
|
||||||
|
} from '../../../src/components/icons/Icon'
|
||||||
|
|
||||||
|
// Create a dummy component for the story
|
||||||
|
const IconShowcase = () => <div />
|
||||||
|
|
||||||
|
const meta: Meta<typeof IconShowcase> = {
|
||||||
|
title: 'Icons/Icon',
|
||||||
|
component: IconShowcase,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered'
|
||||||
|
},
|
||||||
|
tags: ['autodocs']
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
// Predefined Icons
|
||||||
|
export const PredefinedIcons: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 font-semibold">Predefined Icons (Default Size: 1rem)</h3>
|
||||||
|
<div className="grid grid-cols-6 gap-4">
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<CopyIcon />
|
||||||
|
<span className="text-xs text-gray-600">CopyIcon</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<DeleteIcon />
|
||||||
|
<span className="text-xs text-gray-600">DeleteIcon</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<EditIcon />
|
||||||
|
<span className="text-xs text-gray-600">EditIcon</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<RefreshIcon />
|
||||||
|
<span className="text-xs text-gray-600">RefreshIcon</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<ResetIcon />
|
||||||
|
<span className="text-xs text-gray-600">ResetIcon</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<ToolIcon />
|
||||||
|
<span className="text-xs text-gray-600">ToolIcon</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<VisionIcon />
|
||||||
|
<span className="text-xs text-gray-600">VisionIcon</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<WebSearchIcon />
|
||||||
|
<span className="text-xs text-gray-600">WebSearchIcon</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<WrapIcon />
|
||||||
|
<span className="text-xs text-gray-600">WrapIcon</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<UnWrapIcon />
|
||||||
|
<span className="text-xs text-gray-600">UnWrapIcon</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<OcrIcon />
|
||||||
|
<span className="text-xs text-gray-600">OcrIcon</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different Sizes
|
||||||
|
export const DifferentSizes: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<CopyIcon size={12} />
|
||||||
|
<CopyIcon size={16} />
|
||||||
|
<CopyIcon size={20} />
|
||||||
|
<CopyIcon size={24} />
|
||||||
|
<CopyIcon size={32} />
|
||||||
|
<CopyIcon size={48} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-gray-600">
|
||||||
|
<span>12px</span>
|
||||||
|
<span className="ml-2">16px</span>
|
||||||
|
<span className="ml-4">20px</span>
|
||||||
|
<span className="ml-4">24px</span>
|
||||||
|
<span className="ml-6">32px</span>
|
||||||
|
<span className="ml-10">48px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Colors
|
||||||
|
export const CustomColors: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<EditIcon color="#3B82F6" size={24} />
|
||||||
|
<EditIcon color="#10B981" size={24} />
|
||||||
|
<EditIcon color="#F59E0B" size={24} />
|
||||||
|
<EditIcon color="#EF4444" size={24} />
|
||||||
|
<EditIcon color="#8B5CF6" size={24} />
|
||||||
|
<EditIcon color="#EC4899" size={24} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Icon Creation
|
||||||
|
export const CustomIconCreation: Story = {
|
||||||
|
render: () => {
|
||||||
|
// Create custom icons using the factory
|
||||||
|
const SettingsIcon = createIcon(Settings, 24)
|
||||||
|
const DownloadIcon = createIcon(Download, 20)
|
||||||
|
const UploadIcon = createIcon(Upload, 20)
|
||||||
|
const ChevronIcon = createIcon(ChevronRight, 16)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 font-semibold">Custom Icons Created with Factory</h3>
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<SettingsIcon />
|
||||||
|
<span className="text-xs text-gray-600">Settings (24px)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<DownloadIcon />
|
||||||
|
<span className="text-xs text-gray-600">Download (20px)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<UploadIcon />
|
||||||
|
<span className="text-xs text-gray-600">Upload (20px)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<ChevronIcon />
|
||||||
|
<span className="text-xs text-gray-600">Chevron (16px)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 font-semibold">Override Default Size</h3>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<SettingsIcon size={32} />
|
||||||
|
<DownloadIcon size={32} />
|
||||||
|
<UploadIcon size={32} />
|
||||||
|
<ChevronIcon size={32} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon States
|
||||||
|
export const IconStates: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button className="rounded p-2 hover:bg-gray-100">
|
||||||
|
<EditIcon size={20} />
|
||||||
|
</button>
|
||||||
|
<button className="rounded p-2 hover:bg-gray-100" disabled>
|
||||||
|
<EditIcon size={20} className="opacity-50" />
|
||||||
|
</button>
|
||||||
|
<button className="rounded bg-blue-500 p-2 text-white hover:bg-blue-600">
|
||||||
|
<EditIcon size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 text-xs text-gray-600">
|
||||||
|
<span>Normal</span>
|
||||||
|
<span>Disabled</span>
|
||||||
|
<span>Active</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// In Context
|
||||||
|
export const InContext: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="rounded-lg border border-gray-200 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-semibold">Document.pdf</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button className="rounded p-1 hover:bg-gray-100">
|
||||||
|
<CopyIcon size={16} />
|
||||||
|
</button>
|
||||||
|
<button className="rounded p-1 hover:bg-gray-100">
|
||||||
|
<EditIcon size={16} />
|
||||||
|
</button>
|
||||||
|
<button className="rounded p-1 hover:bg-gray-100">
|
||||||
|
<DeleteIcon size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-gray-200 p-4">
|
||||||
|
<div className="mb-3 flex items-center gap-2">
|
||||||
|
<VisionIcon size={20} />
|
||||||
|
<span className="font-medium">Image Processing</span>
|
||||||
|
</div>
|
||||||
|
<p className="mb-3 text-sm text-gray-600">Process your images with advanced AI tools</p>
|
||||||
|
<button className="flex items-center gap-2 rounded bg-blue-500 px-3 py-1 text-sm text-white hover:bg-blue-600">
|
||||||
|
<OcrIcon size={16} />
|
||||||
|
<span>Extract Text</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-gray-200 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm">Auto-refresh</span>
|
||||||
|
<RefreshIcon size={18} className="animate-spin text-blue-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon Grid
|
||||||
|
export const IconGrid: Story = {
|
||||||
|
render: () => {
|
||||||
|
const AllIcons = [
|
||||||
|
{ Icon: CopyIcon, name: 'Copy' },
|
||||||
|
{ Icon: DeleteIcon, name: 'Delete' },
|
||||||
|
{ Icon: EditIcon, name: 'Edit' },
|
||||||
|
{ Icon: RefreshIcon, name: 'Refresh' },
|
||||||
|
{ Icon: ResetIcon, name: 'Reset' },
|
||||||
|
{ Icon: ToolIcon, name: 'Tool' },
|
||||||
|
{ Icon: VisionIcon, name: 'Vision' },
|
||||||
|
{ Icon: WebSearchIcon, name: 'Search' },
|
||||||
|
{ Icon: WrapIcon, name: 'Wrap' },
|
||||||
|
{ Icon: UnWrapIcon, name: 'Unwrap' },
|
||||||
|
{ Icon: OcrIcon, name: 'OCR' }
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-6 gap-4">
|
||||||
|
{AllIcons.map(({ Icon, name }) => (
|
||||||
|
<div
|
||||||
|
key={name}
|
||||||
|
className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4 hover:border-blue-500"
|
||||||
|
>
|
||||||
|
<Icon size={24} />
|
||||||
|
<span className="text-xs">{name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,57 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||||
import { action } from 'storybook/actions'
|
import { action } from 'storybook/actions'
|
||||||
|
|
||||||
import CodeEditor, { getCmThemeByName, getCmThemeNames } from '../../../src/components/interactive/CodeEditor'
|
import CodeEditor, {
|
||||||
|
getCmThemeByName,
|
||||||
|
getCmThemeNames,
|
||||||
|
LanguageConfig
|
||||||
|
} from '../../../src/components/interactive/CodeEditor'
|
||||||
|
|
||||||
|
// 示例语言配置 - 为 Storybook 提供更丰富的语言支持演示
|
||||||
|
const exampleLanguageConfig: LanguageConfig = {
|
||||||
|
JavaScript: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.js', '.mjs', '.cjs'],
|
||||||
|
aliases: ['js', 'node']
|
||||||
|
},
|
||||||
|
TypeScript: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.ts'],
|
||||||
|
aliases: ['ts']
|
||||||
|
},
|
||||||
|
Python: {
|
||||||
|
type: 'programming',
|
||||||
|
extensions: ['.py'],
|
||||||
|
aliases: ['python3', 'py']
|
||||||
|
},
|
||||||
|
JSON: {
|
||||||
|
type: 'data',
|
||||||
|
extensions: ['.json']
|
||||||
|
},
|
||||||
|
Markdown: {
|
||||||
|
type: 'prose',
|
||||||
|
extensions: ['.md', '.markdown'],
|
||||||
|
aliases: ['md']
|
||||||
|
},
|
||||||
|
HTML: {
|
||||||
|
type: 'markup',
|
||||||
|
extensions: ['.html', '.htm']
|
||||||
|
},
|
||||||
|
CSS: {
|
||||||
|
type: 'markup',
|
||||||
|
extensions: ['.css']
|
||||||
|
},
|
||||||
|
'Graphviz (DOT)': {
|
||||||
|
type: 'data',
|
||||||
|
extensions: ['.dot', '.gv'],
|
||||||
|
aliases: ['dot', 'graphviz']
|
||||||
|
},
|
||||||
|
Mermaid: {
|
||||||
|
type: 'markup',
|
||||||
|
extensions: ['.mmd', '.mermaid'],
|
||||||
|
aliases: ['mmd']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const meta: Meta<typeof CodeEditor> = {
|
const meta: Meta<typeof CodeEditor> = {
|
||||||
title: 'Interactive/CodeEditor',
|
title: 'Interactive/CodeEditor',
|
||||||
@ -11,7 +61,7 @@ const meta: Meta<typeof CodeEditor> = {
|
|||||||
argTypes: {
|
argTypes: {
|
||||||
language: {
|
language: {
|
||||||
control: 'select',
|
control: 'select',
|
||||||
options: ['typescript', 'javascript', 'json', 'markdown', 'python', 'dot', 'mmd']
|
options: ['typescript', 'javascript', 'json', 'markdown', 'python', 'dot', 'mmd', 'go', 'rust', 'php']
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
control: 'select',
|
control: 'select',
|
||||||
@ -23,7 +73,11 @@ const meta: Meta<typeof CodeEditor> = {
|
|||||||
wrapped: { control: 'boolean' },
|
wrapped: { control: 'boolean' },
|
||||||
height: { control: 'text' },
|
height: { control: 'text' },
|
||||||
maxHeight: { control: 'text' },
|
maxHeight: { control: 'text' },
|
||||||
minHeight: { control: 'text' }
|
minHeight: { control: 'text' },
|
||||||
|
languageConfig: {
|
||||||
|
control: false,
|
||||||
|
description: 'Optional language configuration. If not provided, uses built-in defaults.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +100,7 @@ export const Default: Story = {
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={args.value as string}
|
value={args.value as string}
|
||||||
language={args.language as string}
|
language={args.language as string}
|
||||||
|
languageConfig={exampleLanguageConfig}
|
||||||
theme={getCmThemeByName((args as any).theme || 'light')}
|
theme={getCmThemeByName((args as any).theme || 'light')}
|
||||||
fontSize={args.fontSize as number}
|
fontSize={args.fontSize as number}
|
||||||
editable={args.editable as boolean}
|
editable={args.editable as boolean}
|
||||||
@ -79,6 +134,7 @@ export const JSONLint: Story = {
|
|||||||
options={{ lint: true }}
|
options={{ lint: true }}
|
||||||
wrapped
|
wrapped
|
||||||
onChange={action('change')}
|
onChange={action('change')}
|
||||||
|
languageConfig={exampleLanguageConfig}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -97,6 +153,7 @@ export const SaveShortcut: Story = {
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={args.value as string}
|
value={args.value as string}
|
||||||
language={args.language as string}
|
language={args.language as string}
|
||||||
|
languageConfig={exampleLanguageConfig}
|
||||||
theme={getCmThemeByName((args as any).theme || 'light')}
|
theme={getCmThemeByName((args as any).theme || 'light')}
|
||||||
options={{ keymap: true }}
|
options={{ keymap: true }}
|
||||||
onSave={action('save')}
|
onSave={action('save')}
|
||||||
@ -107,3 +164,32 @@ export const SaveShortcut: Story = {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用默认语言配置(展示组件的独立性)
|
||||||
|
export const DefaultLanguageConfig: Story = {
|
||||||
|
args: {
|
||||||
|
language: 'javascript',
|
||||||
|
theme: 'light',
|
||||||
|
value: `// 这个示例使用内置的默认语言配置
|
||||||
|
function fibonacci(n) {
|
||||||
|
if (n <= 1) return n;
|
||||||
|
return fibonacci(n - 1) + fibonacci(n - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(fibonacci(10));`,
|
||||||
|
wrapped: true
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<div className="w-[720px] space-y-3">
|
||||||
|
<CodeEditor
|
||||||
|
value={args.value as string}
|
||||||
|
language={args.language as string}
|
||||||
|
// 注意:这里没有传入 languageConfig,使用默认配置
|
||||||
|
theme={getCmThemeByName((args as any).theme || 'light')}
|
||||||
|
onChange={action('change')}
|
||||||
|
wrapped
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">此示例未传入 languageConfig,使用组件内置的默认语言配置。</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,22 +1,23 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"declaration": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"jsx": "react-jsx",
|
"baseUrl": ".",
|
||||||
"lib": ["DOM", "DOM.Iterable", "ES6"],
|
"declaration": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES6"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"rootDir": "./src",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "ES2020"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"exclude": ["node_modules", "dist", "**/*.test.*", "**/__tests__/**"],
|
||||||
"exclude": ["node_modules", "dist", "**/*.test.*", "**/__tests__/**"]
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,9 @@ export default defineConfig({
|
|||||||
clean: true,
|
clean: true,
|
||||||
dts: true,
|
dts: true,
|
||||||
tsconfig: 'tsconfig.json',
|
tsconfig: 'tsconfig.json',
|
||||||
|
alias: {
|
||||||
|
'@shared': '../shared'
|
||||||
|
},
|
||||||
// 将 HeroUI、Tailwind 和其他 peer dependencies 标记为外部依赖
|
// 将 HeroUI、Tailwind 和其他 peer dependencies 标记为外部依赖
|
||||||
external: [
|
external: [
|
||||||
'react',
|
'react',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user