diff --git a/packages/ui/MIGRATION_STATUS.md b/packages/ui/MIGRATION_STATUS.md index e823e8e07b..ede78d008a 100644 --- a/packages/ui/MIGRATION_STATUS.md +++ b/packages/ui/MIGRATION_STATUS.md @@ -49,33 +49,33 @@ function MyComponent() { ## 迁移概览 - **总组件数**: 236 -- **已迁移**: 46 -- **已重构**: 2 -- **待迁移**: 190 +- **已迁移**: 34 +- **已重构**: 14 +- **待迁移**: 188 ## 组件状态表 | Category | Component Name | Migration Status | Refactoring Status | Description | |----------|----------------|------------------|--------------------|-------------| | **base** | | | | 基础组件 | -| | CopyButton | ✅ | ❌ | 复制按钮 | -| | CustomTag | ✅ | ❌ | 自定义标签 | -| | DividerWithText | ✅ | ❌ | 带文本的分隔线 | -| | EmojiIcon | ✅ | ❌ | 表情图标 | -| | ErrorBoundary | ✅ | ❌ | 错误边界 (通过 props 解耦) | +| | CopyButton | ✅ | ✅ | 复制按钮 | +| | CustomTag | ✅ | ✅ | 自定义标签 | +| | DividerWithText | ✅ | ✅ | 带文本的分隔线 | +| | EmojiIcon | ✅ | ✅ | 表情图标 | +| | ErrorBoundary | ✅ | ✅ | 错误边界 (通过 props 解耦) | | | StatusTag | ✅ | ✅ | 统一状态标签(合并了 ErrorTag、SuccessTag、WarnTag、InfoTag)| -| | IndicatorLight | ✅ | ❌ | 指示灯 | -| | Spinner | ✅ | ❌ | 加载动画 | -| | TextBadge | ✅ | ❌ | 文本徽标 | -| | CustomCollapse | ✅ | ❌ | 自定义折叠面板 | +| | IndicatorLight | ✅ | ✅ | 指示灯 | +| | Spinner | ✅ | ✅ | 加载动画 | +| | TextBadge | ✅ | ✅ | 文本徽标 | +| | CustomCollapse | ✅ | ✅ | 自定义折叠面板 | | **display** | | | | 显示组件 | | | Ellipsis | ✅ | ❌ | 文本省略 | -| | ExpandableText | ✅ | ❌ | 可展开文本 | +| | ExpandableText | ✅ | ✅ | 可展开文本 | | | ThinkingEffect | ✅ | ❌ | 思考效果动画 | -| | EmojiAvatar | ✅ | ❌ | 表情头像 | +| | EmojiAvatar | ✅ | ✅ | 表情头像 | | | ListItem | ✅ | ❌ | 列表项 | | | MaxContextCount | ✅ | ❌ | 最大上下文数显示 | -| | ProviderAvatar | ✅ | ❌ | 提供者头像 | +| | ProviderAvatar | ✅ | ✅ | 提供者头像 | | | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) | | | OGCard | ❌ | ❌ | OG 卡片 | | | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 | diff --git a/packages/ui/MIGRATION_STATUS_EN.md b/packages/ui/MIGRATION_STATUS_EN.md index 78638ce07a..36634b4532 100644 --- a/packages/ui/MIGRATION_STATUS_EN.md +++ b/packages/ui/MIGRATION_STATUS_EN.md @@ -48,35 +48,33 @@ When submitting PRs, please place components in the correct directory based on t ## Migration Overview - **Total Components**: 236 -- **Migrated**: 46 -- **Refactored**: 0 -- **Pending Migration**: 190 +- **Migrated**: 34 +- **Refactored**: 14 +- **Pending Migration**: 188 ## Component Status Table | Category | Component Name | Migration Status | Refactoring Status | Description | |----------|----------------|------------------|--------------------|-------------| | **base** | | | | Base components | -| | CopyButton | ✅ | ❌ | Copy button | -| | CustomTag | ✅ | ❌ | Custom tag | -| | DividerWithText | ✅ | ❌ | Divider with text | -| | EmojiIcon | ✅ | ❌ | Emoji icon | -| | ErrorBoundary | ✅ | ❌ | Error boundary (decoupled via props) | -| | ErrorTag | ✅ | ❌ | Error tag | -| | IndicatorLight | ✅ | ❌ | Indicator light | -| | Spinner | ✅ | ❌ | Loading spinner | -| | SuccessTag | ✅ | ❌ | Success tag | -| | TextBadge | ✅ | ❌ | Text badge | -| | WarnTag | ✅ | ❌ | Warning tag | -| | CustomCollapse | ✅ | ❌ | Custom collapse panel | +| | CopyButton | ✅ | ✅ | Copy button | +| | CustomTag | ✅ | ✅ | Custom tag | +| | DividerWithText | ✅ | ✅ | Divider with text | +| | EmojiIcon | ✅ | ✅ | Emoji icon | +| | ErrorBoundary | ✅ | ✅ | Error boundary (decoupled via props) | +| | StatusTag | ✅ | ✅ | Unified status tag (merged ErrorTag, SuccessTag, WarnTag, InfoTag) | +| | IndicatorLight | ✅ | ✅ | Indicator light | +| | Spinner | ✅ | ✅ | Loading spinner | +| | TextBadge | ✅ | ✅ | Text badge | +| | CustomCollapse | ✅ | ✅ | Custom collapse panel | | **display** | | | | Display components | | | Ellipsis | ✅ | ❌ | Text ellipsis | -| | ExpandableText | ✅ | ❌ | Expandable text | +| | ExpandableText | ✅ | ✅ | Expandable text | | | ThinkingEffect | ✅ | ❌ | Thinking effect animation | -| | EmojiAvatar | ✅ | ❌ | Emoji avatar | +| | EmojiAvatar | ✅ | ✅ | Emoji avatar | | | ListItem | ✅ | ❌ | List item | | | MaxContextCount | ✅ | ❌ | Max context count display | -| | ProviderAvatar | ✅ | ❌ | Provider avatar | +| | ProviderAvatar | ✅ | ✅ | Provider avatar | | | CodeViewer | ❌ | ❌ | Code viewer (external deps) | | | OGCard | ❌ | ❌ | OG card | | | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer | @@ -88,22 +86,11 @@ When submitting PRs, please place components in the correct directory based on t | | Tab/* | ❌ | ❌ | Tab (Redux dependency) | | | TopView | ❌ | ❌ | Top view (window.api dependency) | | **icons** | | | | Icon components | -| | CopyIcon | ✅ | ❌ | Copy icon | -| | DeleteIcon | ✅ | ❌ | Delete icon | -| | EditIcon | ✅ | ❌ | Edit icon | -| | FileIcons | ✅ | ❌ | File icons (includes FileSvgIcon, FilePngIcon) | +| | Icon | ✅ | ✅ | Icon factory function and predefined icons (merged CopyIcon, DeleteIcon, EditIcon, RefreshIcon, ResetIcon, ToolIcon, VisionIcon, WebSearchIcon, WrapIcon, UnWrapIcon, OcrIcon) | +| | FileIcons | ✅ | ❌ | File icons (FileSvgIcon, FilePngIcon) | | | ReasoningIcon | ✅ | ❌ | Reasoning icon | -| | RefreshIcon | ✅ | ❌ | Refresh icon | -| | ResetIcon | ✅ | ❌ | Reset icon | -| | SvgSpinners180Ring | ✅ | ❌ | Spinners icon | +| | SvgSpinners180Ring | ✅ | ❌ | Spinner loading icon | | | ToolsCallingIcon | ✅ | ❌ | Tools calling icon | -| | VisionIcon | ✅ | ❌ | Vision icon | -| | WebSearchIcon | ✅ | ❌ | Web search icon | -| | WrapIcon | ✅ | ❌ | Wrap icon | -| | UnWrapIcon | ✅ | ❌ | Unwrap icon | -| | OcrIcon | ✅ | ❌ | OCR icon | -| | ToolIcon | ✅ | ❌ | Tool icon | -| | Other icons | ❌ | ❌ | Other icon files | | **interactive** | | | | Interactive components | | | InfoTooltip | ✅ | ❌ | Info tooltip | | | HelpTooltip | ✅ | ❌ | Help tooltip | diff --git a/packages/ui/src/components/base/CopyButton/index.tsx b/packages/ui/src/components/base/CopyButton/index.tsx index 51e47cfc28..b1eba2b46f 100644 --- a/packages/ui/src/components/base/CopyButton/index.tsx +++ b/packages/ui/src/components/base/CopyButton/index.tsx @@ -1,69 +1,31 @@ // Original path: src/renderer/src/components/CopyButton.tsx -import { Tooltip } from 'antd' +import { Tooltip } from '@heroui/react' import { Copy } from 'lucide-react' import { FC } from 'react' -import styled from 'styled-components' interface CopyButtonProps { tooltip?: string label?: string - color?: string - hoverColor?: string size?: number + className?: string + [key: string]: any } -interface ButtonContainerProps { - $color: string - $hoverColor: string -} - -const CopyButton: FC = ({ - tooltip, - label, - color = 'var(--color-text-2)', - hoverColor = 'var(--color-primary)', - size = 14, - ...props -}) => { +const CopyButton: FC = ({ tooltip, label, size = 14, className = '', ...props }) => { const button = ( - - - {label && {label}} - +
+ + {label && {label}} +
) if (tooltip) { - return {button} + return {button} } return button } -const ButtonContainer = styled.div` - display: flex; - flex-direction: row; - align-items: center; - gap: 4px; - cursor: pointer; - color: ${(props) => props.$color}; - transition: color 0.2s; - - .copy-icon { - color: ${(props) => props.$color}; - transition: color 0.2s; - } - - &:hover { - color: ${(props) => props.$hoverColor}; - - .copy-icon { - color: ${(props) => props.$hoverColor}; - } - } -` - -const RightText = styled.span<{ size: number }>` - font-size: ${(props) => props.size}px; -` - export default CopyButton diff --git a/packages/ui/src/components/base/CustomCollapse/index.tsx b/packages/ui/src/components/base/CustomCollapse/index.tsx index ef5cd8a51e..a334424349 100644 --- a/packages/ui/src/components/base/CustomCollapse/index.tsx +++ b/packages/ui/src/components/base/CustomCollapse/index.tsx @@ -1,23 +1,17 @@ // Original path: src/renderer/src/components/CustomCollapse.tsx -import { Collapse } from 'antd' -import { merge } from 'lodash' import { ChevronRight } from 'lucide-react' -import { FC, memo, useMemo, useState } from 'react' +import { FC, memo, useState } from 'react' interface CustomCollapseProps { label: React.ReactNode - extra: React.ReactNode + extra?: React.ReactNode children: React.ReactNode destroyInactivePanel?: boolean defaultActiveKey?: string[] activeKey?: string[] collapsible?: 'header' | 'icon' | 'disabled' onChange?: (activeKeys: string | string[]) => void - style?: React.CSSProperties - styles?: { - header?: React.CSSProperties - body?: React.CSSProperties - } + className?: string } const CustomCollapse: FC = ({ @@ -29,80 +23,47 @@ const CustomCollapse: FC = ({ activeKey, collapsible = undefined, onChange, - style, - styles + className = '' }) => { - const [activeKeys, setActiveKeys] = useState(activeKey || defaultActiveKey) + const [isOpen, setIsOpen] = useState(activeKey ? activeKey.includes('1') : defaultActiveKey.includes('1')) - const defaultCollapseStyle = { - width: '100%', - background: 'transparent', - border: '0.5px solid var(--color-border)' + const handleToggle = () => { + if (collapsible === 'disabled') return + + const newState = !isOpen + setIsOpen(newState) + onChange?.(newState ? ['1'] : []) } - const defaultCollpaseHeaderStyle = { - padding: '3px 16px', - alignItems: 'center', - justifyContent: 'space-between', - background: 'var(--color-background-soft)' - } - - const getHeaderStyle = () => { - return activeKeys && activeKeys.length > 0 - ? { - ...defaultCollpaseHeaderStyle, - borderTopLeftRadius: '8px', - borderTopRightRadius: '8px' - } - : { - ...defaultCollpaseHeaderStyle, - borderRadius: '8px' - } - } - - const defaultCollapseItemStyles = { - header: getHeaderStyle(), - body: { - borderTop: 'none' - } - } - - const collapseStyle = merge({}, defaultCollapseStyle, style) - const collapseItemStyles = useMemo(() => { - return merge({}, defaultCollapseItemStyles, styles) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeKeys]) + const shouldRenderContent = !destroyInactivePanel || isOpen return ( - { - setActiveKeys(keys) - onChange?.(keys) - }} - expandIcon={({ isActive }) => ( - - )} - items={[ - { - styles: collapseItemStyles, - key: '1', - label, - extra, - children - } - ]} - /> +
+
+
+
+ {(collapsible === 'icon' || collapsible === undefined) && ( +
+ +
+ )} + {label} +
+ {extra &&
{extra}
} +
+
+ {isOpen &&
{shouldRenderContent && children}
} +
) } diff --git a/packages/ui/src/components/base/CustomTag/index.tsx b/packages/ui/src/components/base/CustomTag/index.tsx index a692db0f09..88a8235b5d 100644 --- a/packages/ui/src/components/base/CustomTag/index.tsx +++ b/packages/ui/src/components/base/CustomTag/index.tsx @@ -1,8 +1,7 @@ // Original path: src/renderer/src/components/Tags/CustomTag.tsx -import { CloseOutlined } from '@ant-design/icons' -import { Tooltip } from 'antd' +import { Tooltip } from '@heroui/react' +import { X } from 'lucide-react' import { CSSProperties, FC, memo, MouseEventHandler, useMemo } from 'react' -import styled from 'styled-components' export interface CustomTagProps { icon?: React.ReactNode @@ -16,6 +15,7 @@ export interface CustomTagProps { onClick?: MouseEventHandler disabled?: boolean inactive?: boolean + className?: string } const CustomTag: FC = ({ @@ -29,39 +29,53 @@ const CustomTag: FC = ({ onClose, onClick, disabled, - inactive + inactive, + className = '' }) => { const actualColor = inactive ? '#aaaaaa' : color + const tagContent = useMemo( () => ( - - {icon} {children} + }} + onClick={disabled ? undefined : onClick}> + {icon && {icon}} + {children} {closable && ( - { e.stopPropagation() onClose?.() - }} - /> + }}> + + )} - + ), - [actualColor, children, closable, disabled, icon, onClick, onClose, size, style] + [actualColor, children, closable, disabled, icon, onClick, onClose, size, style, className] ) return tooltip ? ( - + {tagContent} ) : ( @@ -70,49 +84,3 @@ const CustomTag: FC = ({ } export default memo(CustomTag) - -const Tag = styled.div<{ $color: string; $size: number; $closable: boolean; $clickable: boolean }>` - display: inline-flex; - align-items: center; - gap: 4px; - padding: ${({ $size }) => $size / 3}px ${({ $size }) => $size * 0.8}px; - padding-right: ${({ $closable, $size }) => ($closable ? $size * 1.8 : $size * 0.8)}px; - border-radius: 99px; - color: ${({ $color }) => $color}; - background-color: ${({ $color }) => $color + '20'}; - font-size: ${({ $size }) => $size}px; - line-height: 1; - white-space: nowrap; - position: relative; - cursor: ${({ $clickable }) => ($clickable ? 'pointer' : 'auto')}; - .iconfont { - font-size: ${({ $size }) => $size}px; - color: ${({ $color }) => $color}; - } - - transition: opacity 0.2s ease; - &:hover { - opacity: ${({ $clickable }) => ($clickable ? 0.8 : 1)}; - } -` - -const CloseIcon = styled(CloseOutlined)<{ $size: number; $color: string }>` - cursor: pointer; - font-size: ${({ $size }) => $size * 0.8}px; - color: ${({ $color }) => $color}; - display: flex; - align-items: center; - justify-content: center; - position: absolute; - right: ${({ $size }) => $size * 0.2}px; - top: ${({ $size }) => $size * 0.2}px; - bottom: ${({ $size }) => $size * 0.2}px; - border-radius: 99px; - transition: all 0.2s ease; - aspect-ratio: 1; - line-height: 1; - &:hover { - background-color: #da8a8a; - color: #ffffff; - } -` diff --git a/packages/ui/src/components/base/DividerWithText/index.tsx b/packages/ui/src/components/base/DividerWithText/index.tsx index 4359b921b5..5bb5cc5821 100644 --- a/packages/ui/src/components/base/DividerWithText/index.tsx +++ b/packages/ui/src/components/base/DividerWithText/index.tsx @@ -1,37 +1,19 @@ // Original: src/renderer/src/components/DividerWithText.tsx import React, { CSSProperties } from 'react' -import styled from 'styled-components' interface DividerWithTextProps { text: string style?: CSSProperties + className?: string } -const DividerWithText: React.FC = ({ text, style }) => { +const DividerWithText: React.FC = ({ text, style, className = '' }) => { return ( - - {text} - - +
+ {text} +
+
) } -const DividerContainer = styled.div` - display: flex; - align-items: center; - margin: 0px 0; -` - -const DividerText = styled.span` - font-size: 12px; - color: var(--color-text-2); - margin-right: 8px; -` - -const DividerLine = styled.div` - flex: 1; - height: 1px; - background-color: var(--color-border); -` - export default DividerWithText diff --git a/packages/ui/src/components/base/EmojiIcon/index.tsx b/packages/ui/src/components/base/EmojiIcon/index.tsx index f20860bec3..7c3f968a7e 100644 --- a/packages/ui/src/components/base/EmojiIcon/index.tsx +++ b/packages/ui/src/components/base/EmojiIcon/index.tsx @@ -1,6 +1,5 @@ // Original path: src/renderer/src/components/EmojiIcon.tsx import { FC } from 'react' -import styled from 'styled-components' interface EmojiIconProps { emoji: string @@ -9,41 +8,29 @@ interface EmojiIconProps { fontSize?: number } -const EmojiIcon: FC = ({ emoji, className, size = 26, fontSize = 15 }) => { +const EmojiIcon: FC = ({ emoji, className = '', size = 26, fontSize = 15 }) => { return ( - - {emoji || '⭐️'} +
+
+ {emoji || '⭐️'} +
{emoji} - +
) } -const Container = styled.div<{ $size: number; $fontSize: number }>` - width: ${({ $size }) => $size}px; - height: ${({ $size }) => $size}px; - border-radius: ${({ $size }) => $size / 2}px; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - font-size: ${({ $fontSize }) => $fontSize}px; - position: relative; - overflow: hidden; - margin-right: 3px; -` - -const EmojiBackground = styled.div` - width: 100%; - height: 100%; - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - font-size: 200%; - transform: scale(1.5); - filter: blur(5px); - opacity: 0.4; -` - export default EmojiIcon diff --git a/packages/ui/src/components/base/ErrorBoundary/index.tsx b/packages/ui/src/components/base/ErrorBoundary/index.tsx index 26a6780b3e..46fb37e76d 100644 --- a/packages/ui/src/components/base/ErrorBoundary/index.tsx +++ b/packages/ui/src/components/base/ErrorBoundary/index.tsx @@ -1,9 +1,8 @@ // Original path: src/renderer/src/components/ErrorBoundary.tsx -import { Button } from '@heroui/button' -import { Alert, Space } from 'antd' +import { Button } from '@heroui/react' +import { AlertTriangle } from 'lucide-react' import { ComponentType, ReactNode } from 'react' import { ErrorBoundary, FallbackProps } from 'react-error-boundary' -import styled from 'styled-components' import { formatErrorMessage } from './utils' @@ -26,28 +25,29 @@ const DefaultFallback: ComponentType = (props: CustomFallba } = props return ( - - - {onDebugClick && ( - - )} - {onReloadClick && ( - - )} - - } - /> - +
+
+
+ +
+

{errorMessage}

+

{formatErrorMessage(error)}

+
+ {onDebugClick && ( + + )} + {onReloadClick && ( + + )} +
+
+
+
+
) } @@ -89,13 +89,5 @@ const ErrorBoundaryCustomized = ({ ) } -const ErrorContainer = styled.div` - display: flex; - justify-content: center; - align-items: center; - width: 100%; - padding: 8px; -` - export { ErrorBoundaryCustomized as ErrorBoundary } export type { CustomFallbackProps, ErrorBoundaryCustomizedProps } diff --git a/packages/ui/src/components/base/IndicatorLight/index.tsx b/packages/ui/src/components/base/IndicatorLight/index.tsx index 21f2d033ac..0ab947a756 100644 --- a/packages/ui/src/components/base/IndicatorLight/index.tsx +++ b/packages/ui/src/components/base/IndicatorLight/index.tsx @@ -1,6 +1,5 @@ // Original: src/renderer/src/components/IndicatorLight.tsx import React from 'react' -import styled from 'styled-components' interface IndicatorLightProps { color: string @@ -8,38 +7,31 @@ interface IndicatorLightProps { shadow?: boolean style?: React.CSSProperties animation?: boolean + className?: string } -const Light = styled.div<{ - color: string - size: number - shadow?: boolean - style?: React.CSSProperties - animation?: boolean -}>` - width: ${({ size }) => size}px; - height: ${({ size }) => size}px; - border-radius: 50%; - background-color: ${({ color }) => color}; - box-shadow: ${({ shadow, color }) => (shadow ? `0 0 6px ${color}` : 'none')}; - animation: ${({ animation }) => (animation ? 'pulse 2s infinite' : 'none')}; - - @keyframes pulse { - 0% { - opacity: 1; - } - 50% { - opacity: 0.6; - } - 100% { - opacity: 1; - } - } -` - -const IndicatorLight: React.FC = ({ color, size = 8, shadow = true, style, animation = true }) => { +const IndicatorLight: React.FC = ({ + color, + size = 8, + shadow = true, + style, + animation = true, + className = '' +}) => { const actualColor = color === 'green' ? '#22c55e' : color - return + + return ( +
+ ) } export default IndicatorLight diff --git a/packages/ui/src/components/base/Spinner/index.tsx b/packages/ui/src/components/base/Spinner/index.tsx index 1fa5159a7e..c149c2a934 100644 --- a/packages/ui/src/components/base/Spinner/index.tsx +++ b/packages/ui/src/components/base/Spinner/index.tsx @@ -1,10 +1,10 @@ // Original: src/renderer/src/components/Spinner.tsx import { motion } from 'framer-motion' import { Search } from 'lucide-react' -import styled from 'styled-components' interface Props { text: React.ReactNode + className?: string } // Define variants for the spinner animation @@ -17,9 +17,10 @@ const spinnerVariants = { } } -export default function Spinner({ text }: Props) { +export default function Spinner({ text, className = '' }: Props) { return ( - {text} - + ) } -const SearchWrapper = styled.div` - display: flex; - align-items: center; - gap: 4px; - /* font-size: 14px; */ - padding: 0px; - /* padding-left: 0; */ -` -const Searching = motion.create(SearchWrapper) diff --git a/packages/ui/src/components/base/StatusTag/index.tsx b/packages/ui/src/components/base/StatusTag/index.tsx index dad73a1956..590c7c88ab 100644 --- a/packages/ui/src/components/base/StatusTag/index.tsx +++ b/packages/ui/src/components/base/StatusTag/index.tsx @@ -11,6 +11,7 @@ export interface StatusTagProps { iconSize?: number icon?: React.ReactNode color?: string + className?: string } const statusConfig: Record = { @@ -20,14 +21,14 @@ const statusConfig: Record = { info: { Icon: InfoIcon, color: '#3B82F6' } // blue-500 } -export const StatusTag: React.FC = ({ type, message, iconSize = 14, icon, color }) => { +export const StatusTag: React.FC = ({ type, message, iconSize = 14, icon, color, className }) => { const config = statusConfig[type] const Icon = config.Icon const finalColor = color || config.color const finalIcon = icon || return ( - + {message} ) diff --git a/packages/ui/src/components/base/TextBadge/index.tsx b/packages/ui/src/components/base/TextBadge/index.tsx index ab49fd3e0c..f3f42a4f16 100644 --- a/packages/ui/src/components/base/TextBadge/index.tsx +++ b/packages/ui/src/components/base/TextBadge/index.tsx @@ -1,23 +1,20 @@ // Original: src/renderer/src/components/TextBadge.tsx import { FC } from 'react' -import styled from 'styled-components' -interface Props { +interface TextBadgeProps { text: string style?: React.CSSProperties + className?: string } -const TextBadge: FC = ({ text, style }) => { - return {text} +const TextBadge: FC = ({ text, style, className = '' }) => { + return ( + + {text} + + ) } -const Container = styled.span` - font-size: 12px; - color: var(--color-primary); - background: var(--color-primary-bg); - padding: 2px 6px; - border-radius: 4px; - font-weight: 500; -` - export default TextBadge diff --git a/packages/ui/src/components/display/EmojiAvatar/index.tsx b/packages/ui/src/components/display/EmojiAvatar/index.tsx index 2cc7bc925e..1386a889b2 100644 --- a/packages/ui/src/components/display/EmojiAvatar/index.tsx +++ b/packages/ui/src/components/display/EmojiAvatar/index.tsx @@ -1,6 +1,5 @@ // Original path: src/renderer/src/components/Avatar/EmojiAvatar.tsx -import React, { memo } from 'react' -import styled from 'styled-components' +import { memo } from 'react' interface EmojiAvatarProps { children: string @@ -9,46 +8,28 @@ interface EmojiAvatarProps { onClick?: React.MouseEventHandler className?: string style?: React.CSSProperties + ref?: React.RefObject } -const EmojiAvatar = ({ - ref, - children, - size = 31, - fontSize, - onClick, - className, - style -}: EmojiAvatarProps & { ref?: React.RefObject }) => ( - - {children} - -) +const EmojiAvatar = ({ children, size = 31, fontSize, onClick, className = '', style, ref }: EmojiAvatarProps) => { + const computedFontSize = fontSize ?? size * 0.5 + + return ( +
+ {children} +
+ ) +} EmojiAvatar.displayName = 'EmojiAvatar' -const StyledEmojiAvatar = styled.div<{ $size: number; $fontSize: number }>` - display: flex; - align-items: center; - justify-content: center; - background-color: var(--color-background-soft); - border: 0.5px solid var(--color-border); - border-radius: 20%; - cursor: pointer; - width: ${(props) => props.$size}px; - height: ${(props) => props.$size}px; - font-size: ${(props) => props.$fontSize}px; - transition: opacity 0.3s ease; - - &:hover { - opacity: 0.8; - } -` - export default memo(EmojiAvatar) diff --git a/packages/ui/src/components/display/ExpandableText/index.tsx b/packages/ui/src/components/display/ExpandableText/index.tsx index c8010bf698..6d7d677219 100644 --- a/packages/ui/src/components/display/ExpandableText/index.tsx +++ b/packages/ui/src/components/display/ExpandableText/index.tsx @@ -1,52 +1,59 @@ // Original: src/renderer/src/components/ExpandableText.tsx -import { Button } from 'antd' -import { memo, useCallback, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' +import { Button } from '@heroui/react' +import { memo, useCallback, useState } from 'react' interface ExpandableTextProps { text: string style?: React.CSSProperties + className?: string + expandText?: string + collapseText?: string + lineClamp?: number + ref?: React.RefObject } const ExpandableText = ({ - ref, text, - style -}: ExpandableTextProps & { ref?: React.RefObject | null }) => { - const { t } = useTranslation() - const [isExpanded, setIsExpanded] = useState(false) + style, + className = '', + expandText = 'Expand', + collapseText = 'Collapse', + lineClamp = 1, + ref +}: ExpandableTextProps) => { + const [isExpanded, setIsExpanded] = useState(false) - const toggleExpand = useCallback(() => { - setIsExpanded((prev) => !prev) - }, []) + const toggleExpand = useCallback(() => { + setIsExpanded((prev) => !prev) + }, []) - const button = useMemo(() => { return ( - +
+
+ {text} +
+ +
) - }, [isExpanded, t, toggleExpand]) + } - return ( - - {text} - {button} - - ) -} - -const Container = styled.div<{ $expanded?: boolean }>` - display: flex; - flex-direction: ${(props) => (props.$expanded ? 'column' : 'row')}; -` - -const TextContainer = styled.div<{ $expanded?: boolean }>` - overflow: hidden; - text-overflow: ${(props) => (props.$expanded ? 'unset' : 'ellipsis')}; - white-space: ${(props) => (props.$expanded ? 'normal' : 'nowrap')}; - line-height: ${(props) => (props.$expanded ? 'unset' : '30px')}; -` +ExpandableText.displayName = 'ExpandableText' export default memo(ExpandableText) diff --git a/packages/ui/src/components/display/ProviderAvatar/index.tsx b/packages/ui/src/components/display/ProviderAvatar/index.tsx index a61a040780..980ac0254a 100644 --- a/packages/ui/src/components/display/ProviderAvatar/index.tsx +++ b/packages/ui/src/components/display/ProviderAvatar/index.tsx @@ -1,7 +1,6 @@ // Original path: src/renderer/src/components/ProviderAvatar.tsx -import { Avatar } from 'antd' +import { Avatar } from '@heroui/react' import React from 'react' -import styled from 'styled-components' import { generateColorFromChar, getFirstCharacter, getForegroundColor } from './utils' @@ -9,50 +8,55 @@ interface ProviderAvatarProps { providerId: string providerName: string logoSrc?: string - size?: number + size?: 'sm' | 'md' | 'lg' | 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 = ({ providerId, providerName, logoSrc, - size, + size = 'md', className, style, renderCustomLogo }) => { + // Convert numeric size to HeroUI size props + const getAvatarSize = () => { + if (typeof size === 'number') { + // For custom numeric sizes, we'll use style override + return 'md' + } + return size + } + + const getCustomStyle = () => { + const baseStyle: React.CSSProperties = { + ...style + } + + if (typeof size === 'number') { + baseStyle.width = `${size}px` + baseStyle.height = `${size}px` + } + + return baseStyle + } + // Check if custom logo renderer is provided for special providers if (renderCustomLogo) { const customLogo = renderCustomLogo(providerId) if (customLogo) { return ( - - {customLogo} - +
+
+ {customLogo} +
+
) } } @@ -60,7 +64,13 @@ export const ProviderAvatar: React.FC = ({ // If logo source is provided, render image avatar if (logoSrc) { return ( - + ) } @@ -69,17 +79,16 @@ export const ProviderAvatar: React.FC = ({ const color = providerName ? getForegroundColor(backgroundColor) : 'white' return ( - - {getFirstCharacter(providerName)} - + ...getCustomStyle() + }} + /> ) } diff --git a/packages/ui/src/components/display/ThinkingEffect/index.tsx b/packages/ui/src/components/display/ThinkingEffect/index.tsx index fca5e1bd8a..7ad0a1aef8 100644 --- a/packages/ui/src/components/display/ThinkingEffect/index.tsx +++ b/packages/ui/src/components/display/ThinkingEffect/index.tsx @@ -7,14 +7,14 @@ import styled from 'styled-components' import { lightbulbVariants } from './defaultVariants' -interface Props { +interface ThinkingEffectProps { isThinking: boolean thinkingTimeText: React.ReactNode content: string expanded: boolean } -const ThinkingEffect: React.FC = ({ isThinking, thinkingTimeText, content, expanded }) => { +const ThinkingEffect: React.FC = ({ isThinking, thinkingTimeText, content, expanded }) => { const [messages, setMessages] = useState([]) useEffect(() => { diff --git a/packages/ui/stories/components/base/CopyButton.stories.tsx b/packages/ui/stories/components/base/CopyButton.stories.tsx new file mode 100644 index 0000000000..fbe0b2faaf --- /dev/null +++ b/packages/ui/stories/components/base/CopyButton.stories.tsx @@ -0,0 +1,123 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import CopyButton from '../../../src/components/base/CopyButton' + +const meta: Meta = { + title: 'Base/CopyButton', + component: CopyButton, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + argTypes: { + tooltip: { + control: 'text', + description: '悬停时显示的提示文字' + }, + label: { + control: 'text', + description: '复制按钮的标签文字' + }, + size: { + control: { type: 'range', min: 10, max: 30, step: 1 }, + description: '图标和文字的大小' + }, + className: { + control: 'text', + description: '自定义 CSS 类名' + } + } +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {} +} + +export const WithTooltip: Story = { + args: { + tooltip: '点击复制' + } +} + +export const WithLabel: Story = { + args: { + label: '复制' + } +} + +export const WithTooltipAndLabel: Story = { + args: { + tooltip: '点击复制内容到剪贴板', + label: '复制内容' + } +} + +export const SmallSize: Story = { + args: { + size: 12, + label: '小尺寸', + tooltip: '小尺寸复制按钮' + } +} + +export const LargeSize: Story = { + args: { + size: 20, + label: '大尺寸', + tooltip: '大尺寸复制按钮' + } +} + +export const CustomStyle: Story = { + args: { + label: '自定义样式', + tooltip: '自定义样式的复制按钮', + className: 'bg-blue-50 dark:bg-blue-900/20 p-2 rounded-lg border-2 border-blue-200 dark:border-blue-700' + } +} + +export const OnlyIcon: Story = { + args: { + tooltip: '仅图标模式', + size: 16 + } +} + +export const Interactive: Story = { + args: { + tooltip: '可交互的复制按钮', + label: '点击复制' + }, + render: (args) => ( +
+
+

不同状态的复制按钮:

+
+
+ alert('已复制!')} /> +
+
+ +
+
+
+
+ ) +} + +export const MultipleButtons: Story = { + render: () => ( +
+

多个复制按钮组合:

+
+ + + + +
+
+ ) +} diff --git a/packages/ui/stories/components/base/CustomCollapse.stories.tsx b/packages/ui/stories/components/base/CustomCollapse.stories.tsx new file mode 100644 index 0000000000..e6a710f00d --- /dev/null +++ b/packages/ui/stories/components/base/CustomCollapse.stories.tsx @@ -0,0 +1,284 @@ +import { Button } from '@heroui/react' +import type { Meta, StoryObj } from '@storybook/react' +import { AlertTriangle, Info, Settings } from 'lucide-react' +import { useState } from 'react' + +import CustomCollapse from '../../../src/components/base/CustomCollapse' + +const meta: Meta = { + title: 'Base/CustomCollapse', + component: CustomCollapse, + parameters: { + layout: 'padded' + }, + tags: ['autodocs'], + argTypes: { + label: { + control: false, + description: '折叠面板的标题内容' + }, + extra: { + control: false, + description: '标题栏右侧的额外内容' + }, + children: { + control: false, + description: '折叠面板的内容' + }, + destroyInactivePanel: { + control: 'boolean', + description: '是否销毁非活动面板的内容' + }, + defaultActiveKey: { + control: false, + description: '默认激活的面板键值' + }, + activeKey: { + control: false, + description: '当前激活的面板键值(受控模式)' + }, + collapsible: { + control: 'select', + options: ['header', 'icon', 'disabled', undefined], + description: '折叠触发方式' + }, + onChange: { + control: false, + description: '面板状态变化回调' + } + } +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + label: '默认折叠面板', + children: ( +
+

这是折叠面板的内容。

+

可以包含任何内容,包括文本、图片、表单等。

+
+ ) + } +} + +export const WithExtra: Story = { + args: { + label: '带额外内容的面板', + extra: ( + + ), + children: ( +
+

这个面板在标题栏右侧有一个额外的按钮。

+

额外内容不会触发折叠/展开操作。

+
+ ) + } +} + +export const WithIcon: Story = { + args: { + label: ( +
+ + 设置面板 +
+ ), + children: ( +
+
+
+ 通知设置 + +
+
+ 自动更新 + +
+
+
+ ) + } +} + +export const CollapsibleHeader: Story = { + args: { + label: '点击整个标题栏展开/收起', + collapsible: 'header', + children: ( +
+

通过设置 collapsible="header",点击整个标题栏都可以触发折叠/展开。

+
+ ) + } +} + +export const CollapsibleIcon: Story = { + args: { + label: '仅点击图标展开/收起', + collapsible: 'icon', + children: ( +
+

通过设置 collapsible="icon",只有点击左侧的箭头图标才能触发折叠/展开。

+
+ ) + } +} + +export const Disabled: Story = { + args: { + label: '禁用的折叠面板', + collapsible: 'disabled', + children: ( +
+

这个面板被禁用了,无法折叠或展开。

+
+ ) + } +} + +export const DestroyInactivePanel: Story = { + args: { + label: '销毁非活动内容', + destroyInactivePanel: true, + children: ( +
+

当 destroyInactivePanel=true 时,面板收起时会销毁内容,展开时重新渲染。

+

当前时间:{new Date().toLocaleTimeString()}

+
+ ) + } +} + +export const RichContent: Story = { + args: { + label: ( +
+ + 详细信息 +
+ ), + extra: ( +
+ + +
+ ), + children: ( +
+
+

基本信息

+
+
+ + +
+
+ + +
+
+
+
+ +