mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +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
|
||||
- **已迁移**: 43
|
||||
- **已重构**: 0
|
||||
- **待迁移**: 193
|
||||
- **已迁移**: 46
|
||||
- **已重构**: 2
|
||||
- **待迁移**: 190
|
||||
|
||||
## 组件状态表
|
||||
|
||||
@ -62,12 +62,11 @@ function MyComponent() {
|
||||
| | CustomTag | ✅ | ❌ | 自定义标签 |
|
||||
| | DividerWithText | ✅ | ❌ | 带文本的分隔线 |
|
||||
| | EmojiIcon | ✅ | ❌ | 表情图标 |
|
||||
| | ErrorTag | ✅ | ❌ | 错误标签 |
|
||||
| | ErrorBoundary | ✅ | ❌ | 错误边界 (通过 props 解耦) |
|
||||
| | StatusTag | ✅ | ✅ | 统一状态标签(合并了 ErrorTag、SuccessTag、WarnTag、InfoTag)|
|
||||
| | IndicatorLight | ✅ | ❌ | 指示灯 |
|
||||
| | Spinner | ✅ | ❌ | 加载动画 |
|
||||
| | SuccessTag | ✅ | ❌ | 成功标签 |
|
||||
| | TextBadge | ✅ | ❌ | 文本徽标 |
|
||||
| | WarnTag | ✅ | ❌ | 警告标签 |
|
||||
| | CustomCollapse | ✅ | ❌ | 自定义折叠面板 |
|
||||
| **display** | | | | 显示组件 |
|
||||
| | Ellipsis | ✅ | ❌ | 文本省略 |
|
||||
@ -76,6 +75,7 @@ function MyComponent() {
|
||||
| | EmojiAvatar | ✅ | ❌ | 表情头像 |
|
||||
| | ListItem | ✅ | ❌ | 列表项 |
|
||||
| | MaxContextCount | ✅ | ❌ | 最大上下文数显示 |
|
||||
| | ProviderAvatar | ✅ | ❌ | 提供者头像 |
|
||||
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
|
||||
| | OGCard | ❌ | ❌ | OG 卡片 |
|
||||
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 |
|
||||
@ -87,22 +87,11 @@ function MyComponent() {
|
||||
| | Tab/* | ❌ | ❌ | 标签页 (Redux 依赖) |
|
||||
| | TopView | ❌ | ❌ | 顶部视图 (window.api 依赖) |
|
||||
| **icons** | | | | 图标组件 |
|
||||
| | CopyIcon | ✅ | ❌ | 复制图标 |
|
||||
| | DeleteIcon | ✅ | ❌ | 删除图标 |
|
||||
| | EditIcon | ✅ | ❌ | 编辑图标 |
|
||||
| | FileIcons | ✅ | ❌ | 文件图标 (包含 FileSvgIcon、FilePngIcon) |
|
||||
| | Icon | ✅ | ✅ | 图标工厂函数和预定义图标(合并了 CopyIcon、DeleteIcon、EditIcon、RefreshIcon、ResetIcon、ToolIcon、VisionIcon、WebSearchIcon、WrapIcon、UnWrapIcon、OcrIcon)|
|
||||
| | FileIcons | ✅ | ❌ | 文件图标 (FileSvgIcon、FilePngIcon) |
|
||||
| | ReasoningIcon | ✅ | ❌ | 推理图标 |
|
||||
| | RefreshIcon | ✅ | ❌ | 刷新图标 |
|
||||
| | ResetIcon | ✅ | ❌ | 重置图标 |
|
||||
| | SvgSpinners180Ring | ✅ | ❌ | 旋转加载图标 |
|
||||
| | ToolsCallingIcon | ✅ | ❌ | 工具调用图标 |
|
||||
| | VisionIcon | ✅ | ❌ | 视觉图标 |
|
||||
| | WebSearchIcon | ✅ | ❌ | 网页搜索图标 |
|
||||
| | WrapIcon | ✅ | ❌ | 换行图标 |
|
||||
| | UnWrapIcon | ✅ | ❌ | 不换行图标 |
|
||||
| | OcrIcon | ✅ | ❌ | OCR 图标 |
|
||||
| | ToolIcon | ✅ | ❌ | 工具图标 |
|
||||
| | Other icons | ❌ | ❌ | 其他图标文件 |
|
||||
| **interactive** | | | | 交互组件 |
|
||||
| | InfoTooltip | ✅ | ❌ | 信息提示 |
|
||||
| | HelpTooltip | ✅ | ❌ | 帮助提示 |
|
||||
@ -112,6 +101,7 @@ function MyComponent() {
|
||||
| | CollapsibleSearchBar | ✅ | ❌ | 可折叠搜索栏 |
|
||||
| | ImageToolButton | ✅ | ❌ | 图片工具按钮 |
|
||||
| | DraggableList | ✅ | ❌ | 可拖拽列表 |
|
||||
| | CodeEditor | ✅ | ❌ | 代码编辑器 |
|
||||
| | EmojiPicker | ❌ | ❌ | 表情选择器 (useTheme 依赖) |
|
||||
| | Selector | ✅ | ❌ | 选择器 (i18n 依赖) |
|
||||
| | ModelSelector | ❌ | ❌ | 模型选择器 (Redux 依赖) |
|
||||
@ -122,7 +112,6 @@ function MyComponent() {
|
||||
| **未分类** | | | | 需要分类的组件 |
|
||||
| | Popups/* (16+ 文件) | ❌ | ❌ | 弹窗组件 (业务耦合) |
|
||||
| | RichEditor/* (30+ 文件) | ❌ | ❌ | 富文本编辑器 |
|
||||
| | CodeEditor/* | ❌ | ❌ | 代码编辑器 |
|
||||
| | MarkdownEditor/* | ❌ | ❌ | Markdown 编辑器 |
|
||||
| | MinApp/* | ❌ | ❌ | 迷你应用 (Redux 依赖) |
|
||||
| | Avatar/* | ❌ | ❌ | 头像组件 |
|
||||
|
||||
@ -48,9 +48,9 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
## Migration Overview
|
||||
|
||||
- **Total Components**: 236
|
||||
- **Migrated**: 43
|
||||
- **Migrated**: 46
|
||||
- **Refactored**: 0
|
||||
- **Pending Migration**: 193
|
||||
- **Pending Migration**: 190
|
||||
|
||||
## Component Status Table
|
||||
|
||||
@ -61,6 +61,7 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
| | CustomTag | ✅ | ❌ | Custom tag |
|
||||
| | DividerWithText | ✅ | ❌ | Divider with text |
|
||||
| | EmojiIcon | ✅ | ❌ | Emoji icon |
|
||||
| | ErrorBoundary | ✅ | ❌ | Error boundary (decoupled via props) |
|
||||
| | ErrorTag | ✅ | ❌ | Error tag |
|
||||
| | IndicatorLight | ✅ | ❌ | Indicator light |
|
||||
| | Spinner | ✅ | ❌ | Loading spinner |
|
||||
@ -75,6 +76,7 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
| | 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 |
|
||||
@ -111,6 +113,7 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
| | 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) |
|
||||
@ -121,7 +124,6 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
| **Uncategorized** | | | | Components needing categorization |
|
||||
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
|
||||
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
|
||||
| | CodeEditor/* | ❌ | ❌ | Code editor |
|
||||
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
|
||||
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
|
||||
| | Avatar/* | ❌ | ❌ | Avatar components |
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
"lint": "eslint src --ext .ts,.tsx --fix",
|
||||
"type-check": "tsc --noEmit",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"update:languages": "tsx scripts/update-languages.ts"
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"keywords": [
|
||||
"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 DividerWithText } from './base/DividerWithText'
|
||||
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 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 { WarnTag } from './base/WarnTag'
|
||||
|
||||
// Display Components
|
||||
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 ListItem } from './display/ListItem'
|
||||
export { default as MaxContextCount } from './display/MaxContextCount'
|
||||
export { ProviderAvatar } from './display/ProviderAvatar'
|
||||
export { default as ThinkingEffect } from './display/ThinkingEffect'
|
||||
|
||||
// Layout Components
|
||||
@ -24,21 +26,25 @@ export { default as HorizontalScrollContainer } from './layout/HorizontalScrollC
|
||||
export { default as Scrollbar } from './layout/Scrollbar'
|
||||
|
||||
// Icon Components
|
||||
export { default as CopyIcon } from './icons/CopyIcon'
|
||||
export { default as DeleteIcon } from './icons/DeleteIcon'
|
||||
export { default as EditIcon } from './icons/EditIcon'
|
||||
export {
|
||||
createIcon,
|
||||
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 { default as OcrIcon } from './icons/OcrIcon'
|
||||
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 ToolIcon } from './icons/ToolIcon'
|
||||
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
|
||||
export {
|
||||
|
||||
@ -15,6 +15,7 @@ const CodeEditor = ({
|
||||
value,
|
||||
placeholder,
|
||||
language,
|
||||
languageConfig,
|
||||
onSave,
|
||||
onChange,
|
||||
onBlur,
|
||||
@ -55,7 +56,7 @@ const CodeEditor = ({
|
||||
const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? ''))
|
||||
const editorViewRef = useRef<EditorView | null>(null)
|
||||
|
||||
const langExtensions = useLanguageExtensions(language, options?.lint)
|
||||
const langExtensions = useLanguageExtensions(language, options?.lint, languageConfig)
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const currentDoc = editorViewRef.current?.state.doc.toString() ?? ''
|
||||
|
||||
@ -3,6 +3,7 @@ import { EditorView } from '@codemirror/view'
|
||||
import { Extension, keymap } from '@uiw/react-codemirror'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { LanguageConfig } from './types'
|
||||
import { getNormalizedExtension } from './utils'
|
||||
|
||||
/** 语言对应的 linter 加载器
|
||||
@ -34,8 +35,8 @@ const specialLanguageLoaders: Record<string, () => Promise<Extension>> = {
|
||||
/**
|
||||
* 加载语言扩展
|
||||
*/
|
||||
async function loadLanguageExtension(language: string): Promise<Extension | null> {
|
||||
const fileExt = await getNormalizedExtension(language)
|
||||
async function loadLanguageExtension(language: string, languageConfig?: LanguageConfig): Promise<Extension | null> {
|
||||
const fileExt = await getNormalizedExtension(language, languageConfig)
|
||||
|
||||
// 尝试加载特殊语言
|
||||
const specialLoader = specialLanguageLoaders[fileExt]
|
||||
@ -62,8 +63,8 @@ async function loadLanguageExtension(language: string): Promise<Extension | null
|
||||
/**
|
||||
* 加载 linter 扩展
|
||||
*/
|
||||
async function loadLinterExtension(language: string): Promise<Extension | null> {
|
||||
const fileExt = await getNormalizedExtension(language)
|
||||
async function loadLinterExtension(language: string, languageConfig?: LanguageConfig): Promise<Extension | null> {
|
||||
const fileExt = await getNormalizedExtension(language, languageConfig)
|
||||
|
||||
const loader = linterLoaders[fileExt]
|
||||
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[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
@ -89,8 +90,8 @@ export const useLanguageExtensions = (language: string, lint?: boolean) => {
|
||||
try {
|
||||
// 加载所有扩展
|
||||
const [languageResult, linterResult] = await Promise.allSettled([
|
||||
loadLanguageExtension(language),
|
||||
lint ? loadLinterExtension(language) : Promise.resolve(null)
|
||||
loadLanguageExtension(language, languageConfig),
|
||||
lint ? loadLinterExtension(language, languageConfig) : Promise.resolve(null)
|
||||
])
|
||||
|
||||
if (cancelled) return
|
||||
@ -121,7 +122,7 @@ export const useLanguageExtensions = (language: string, lint?: boolean) => {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [language, lint])
|
||||
}, [language, lint, languageConfig])
|
||||
|
||||
return extensions
|
||||
}
|
||||
|
||||
@ -2,6 +2,16 @@ import { BasicSetupOptions, Extension } from '@uiw/react-codemirror'
|
||||
|
||||
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 {
|
||||
save?: () => void
|
||||
}
|
||||
@ -20,6 +30,12 @@ export interface CodeEditorProps {
|
||||
* - Supports file extensions: .cpp/cpp, .js/js, .py/py, etc.
|
||||
*/
|
||||
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. */
|
||||
onSave?: (newContent: string) => void
|
||||
/** 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 diff from 'fast-diff'
|
||||
|
||||
import { getExtensionByLanguage } from '../../../utils/codeLanguage'
|
||||
import { CodeMirrorTheme } from './types'
|
||||
import { CodeMirrorTheme, LanguageConfig } from './types'
|
||||
|
||||
/**
|
||||
* Computes code changes using fast-diff and converts them to CodeMirror changes.
|
||||
@ -50,15 +49,158 @@ const _customLanguageExtensions: Record<string, string> = {
|
||||
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
|
||||
* - 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
|
||||
* @param language language name
|
||||
* @param languageConfig optional language configuration
|
||||
* @returns file extension (without `.` prefix)
|
||||
*/
|
||||
export async function getNormalizedExtension(language: string) {
|
||||
export async function getNormalizedExtension(language: string, languageConfig?: LanguageConfig) {
|
||||
let lang = language
|
||||
|
||||
// If the language name looks like an extension, remove the dot
|
||||
@ -74,8 +216,8 @@ export async function getNormalizedExtension(language: string) {
|
||||
return customExt
|
||||
}
|
||||
|
||||
// 2. Search for github linguist extensions
|
||||
const linguistExt = getExtensionByLanguage(lang)
|
||||
// 2. Search for language configuration extensions
|
||||
const linguistExt = getExtensionByLanguage(lang, languageConfig)
|
||||
if (linguistExt) {
|
||||
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 { 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> = {
|
||||
title: 'Interactive/CodeEditor',
|
||||
@ -11,7 +61,7 @@ const meta: Meta<typeof CodeEditor> = {
|
||||
argTypes: {
|
||||
language: {
|
||||
control: 'select',
|
||||
options: ['typescript', 'javascript', 'json', 'markdown', 'python', 'dot', 'mmd']
|
||||
options: ['typescript', 'javascript', 'json', 'markdown', 'python', 'dot', 'mmd', 'go', 'rust', 'php']
|
||||
},
|
||||
theme: {
|
||||
control: 'select',
|
||||
@ -23,7 +73,11 @@ const meta: Meta<typeof CodeEditor> = {
|
||||
wrapped: { control: 'boolean' },
|
||||
height: { 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
|
||||
value={args.value as string}
|
||||
language={args.language as string}
|
||||
languageConfig={exampleLanguageConfig}
|
||||
theme={getCmThemeByName((args as any).theme || 'light')}
|
||||
fontSize={args.fontSize as number}
|
||||
editable={args.editable as boolean}
|
||||
@ -79,6 +134,7 @@ export const JSONLint: Story = {
|
||||
options={{ lint: true }}
|
||||
wrapped
|
||||
onChange={action('change')}
|
||||
languageConfig={exampleLanguageConfig}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@ -97,6 +153,7 @@ export const SaveShortcut: Story = {
|
||||
<CodeEditor
|
||||
value={args.value as string}
|
||||
language={args.language as string}
|
||||
languageConfig={exampleLanguageConfig}
|
||||
theme={getCmThemeByName((args as any).theme || 'light')}
|
||||
options={{ keymap: true }}
|
||||
onSave={action('save')}
|
||||
@ -107,3 +164,32 @@ export const SaveShortcut: Story = {
|
||||
</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": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES6"],
|
||||
"baseUrl": ".",
|
||||
"declaration": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"incremental": 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,
|
||||
dts: true,
|
||||
tsconfig: 'tsconfig.json',
|
||||
alias: {
|
||||
'@shared': '../shared'
|
||||
},
|
||||
// 将 HeroUI、Tailwind 和其他 peer dependencies 标记为外部依赖
|
||||
external: [
|
||||
'react',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user