mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 20:12:38 +08:00
feat: update migration status and add new UI components
- Updated migration status documentation to reflect the migration of 36 components, with 200 pending. - Enhanced the component status table with new entries for CustomCollapse, EmojiAvatar, ResetIcon, OcrIcon, ToolIcon, WrapIcon, UnWrapIcon, HelpTooltip, Selector, and WarnTooltip. - Updated index.ts to export the newly added components for improved accessibility.
This commit is contained in:
parent
1c27481813
commit
6eb9ab30b0
@ -49,9 +49,9 @@ function MyComponent() {
|
||||
## 迁移概览
|
||||
|
||||
- **总组件数**: 236
|
||||
- **已迁移**: 26
|
||||
- **已迁移**: 36
|
||||
- **已重构**: 0
|
||||
- **待迁移**: 210
|
||||
- **待迁移**: 200
|
||||
|
||||
## 组件状态表
|
||||
|
||||
@ -68,11 +68,12 @@ function MyComponent() {
|
||||
| | SuccessTag | ✅ | ❌ | 成功标签 |
|
||||
| | TextBadge | ✅ | ❌ | 文本徽标 |
|
||||
| | WarnTag | ✅ | ❌ | 警告标签 |
|
||||
| | CustomCollapse | ❌ | ❌ | 自定义折叠面板 |
|
||||
| | CustomCollapse | ✅ | ❌ | 自定义折叠面板 |
|
||||
| **display** | | | | 显示组件 |
|
||||
| | Ellipsis | ✅ | ❌ | 文本省略 |
|
||||
| | ExpandableText | ✅ | ❌ | 可展开文本 |
|
||||
| | ThinkingEffect | ✅ | ❌ | 思考效果动画 |
|
||||
| | EmojiAvatar | ✅ | ❌ | 表情头像 |
|
||||
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
|
||||
| | OGCard | ❌ | ❌ | OG 卡片 |
|
||||
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 |
|
||||
@ -90,19 +91,24 @@ function MyComponent() {
|
||||
| | FileIcons | ✅ | ❌ | 文件图标 (包含 FileSvgIcon、FilePngIcon) |
|
||||
| | ReasoningIcon | ✅ | ❌ | 推理图标 |
|
||||
| | RefreshIcon | ✅ | ❌ | 刷新图标 |
|
||||
| | ResetIcon | ✅ | ❌ | 重置图标 |
|
||||
| | SvgSpinners180Ring | ✅ | ❌ | 旋转加载图标 |
|
||||
| | ToolsCallingIcon | ✅ | ❌ | 工具调用图标 |
|
||||
| | VisionIcon | ✅ | ❌ | 视觉图标 |
|
||||
| | WebSearchIcon | ✅ | ❌ | 网页搜索图标 |
|
||||
| | WrapIcon | ✅ | ❌ | 换行图标 |
|
||||
| | UnWrapIcon | ✅ | ❌ | 不换行图标 |
|
||||
| | OcrIcon | ✅ | ❌ | OCR 图标 |
|
||||
| | ToolIcon | ✅ | ❌ | 工具图标 |
|
||||
| | Other icons | ❌ | ❌ | 其他图标文件 |
|
||||
| **interactive** | | | | 交互组件 |
|
||||
| | InfoTooltip | ✅ | ❌ | 信息提示 |
|
||||
| | HelpTooltip | ❌ | ❌ | 帮助提示 |
|
||||
| | WarnTooltip | ❌ | ❌ | 警告提示 |
|
||||
| | HelpTooltip | ✅ | ❌ | 帮助提示 |
|
||||
| | WarnTooltip | ✅ | ❌ | 警告提示 |
|
||||
| | DraggableList | ❌ | ❌ | 可拖拽列表 |
|
||||
| | EditableNumber | ❌ | ❌ | 可编辑数字 |
|
||||
| | EmojiPicker | ❌ | ❌ | 表情选择器 |
|
||||
| | Selector | ❌ | ❌ | 选择器 |
|
||||
| | Selector | ✅ | ❌ | 选择器 (i18n 依赖) |
|
||||
| | ModelSelector | ❌ | ❌ | 模型选择器 (Redux 依赖) |
|
||||
| | LanguageSelect | ❌ | ❌ | 语言选择 |
|
||||
| | TranslateButton | ❌ | ❌ | 翻译按钮 (window.api 依赖) |
|
||||
|
||||
@ -48,9 +48,9 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
## Migration Overview
|
||||
|
||||
- **Total Components**: 236
|
||||
- **Migrated**: 26
|
||||
- **Migrated**: 36
|
||||
- **Refactored**: 0
|
||||
- **Pending Migration**: 210
|
||||
- **Pending Migration**: 200
|
||||
|
||||
## Component Status Table
|
||||
|
||||
@ -67,11 +67,12 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
| | SuccessTag | ✅ | ❌ | Success tag |
|
||||
| | TextBadge | ✅ | ❌ | Text badge |
|
||||
| | WarnTag | ✅ | ❌ | Warning tag |
|
||||
| | CustomCollapse | ❌ | ❌ | Custom collapse panel |
|
||||
| | CustomCollapse | ✅ | ❌ | Custom collapse panel |
|
||||
| **display** | | | | Display components |
|
||||
| | Ellipsis | ✅ | ❌ | Text ellipsis |
|
||||
| | ExpandableText | ✅ | ❌ | Expandable text |
|
||||
| | ThinkingEffect | ✅ | ❌ | Thinking effect animation |
|
||||
| | EmojiAvatar | ✅ | ❌ | Emoji avatar |
|
||||
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
|
||||
| | OGCard | ❌ | ❌ | OG card |
|
||||
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
|
||||
@ -89,19 +90,24 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
| | FileIcons | ✅ | ❌ | File icons (includes FileSvgIcon, FilePngIcon) |
|
||||
| | ReasoningIcon | ✅ | ❌ | Reasoning icon |
|
||||
| | RefreshIcon | ✅ | ❌ | Refresh icon |
|
||||
| | ResetIcon | ✅ | ❌ | Reset icon |
|
||||
| | SvgSpinners180Ring | ✅ | ❌ | Spinners 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 |
|
||||
| | WarnTooltip | ❌ | ❌ | Warning tooltip |
|
||||
| | HelpTooltip | ✅ | ❌ | Help tooltip |
|
||||
| | WarnTooltip | ✅ | ❌ | Warning tooltip |
|
||||
| | DraggableList | ❌ | ❌ | Draggable list |
|
||||
| | EditableNumber | ❌ | ❌ | Editable number |
|
||||
| | EmojiPicker | ❌ | ❌ | Emoji picker |
|
||||
| | Selector | ❌ | ❌ | Selector |
|
||||
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
|
||||
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
|
||||
| | LanguageSelect | ❌ | ❌ | Language select |
|
||||
| | TranslateButton | ❌ | ❌ | Translate button (window.api dependency) |
|
||||
@ -148,4 +154,4 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
3. **Submission Guidelines**:
|
||||
- Each PR should focus on one category of components
|
||||
- Ensure all migrated components are exported
|
||||
- Update migration status in this document
|
||||
- Update migration status in this document
|
||||
|
||||
109
packages/ui/src/components/base/CustomCollapse/index.tsx
Normal file
109
packages/ui/src/components/base/CustomCollapse/index.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
// 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'
|
||||
|
||||
interface CustomCollapseProps {
|
||||
label: 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
|
||||
}
|
||||
}
|
||||
|
||||
const CustomCollapse: FC<CustomCollapseProps> = ({
|
||||
label,
|
||||
extra,
|
||||
children,
|
||||
destroyInactivePanel = false,
|
||||
defaultActiveKey = ['1'],
|
||||
activeKey,
|
||||
collapsible = undefined,
|
||||
onChange,
|
||||
style,
|
||||
styles
|
||||
}) => {
|
||||
const [activeKeys, setActiveKeys] = useState(activeKey || defaultActiveKey)
|
||||
|
||||
const defaultCollapseStyle = {
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: '0.5px solid var(--color-border)'
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
bordered={false}
|
||||
style={collapseStyle}
|
||||
defaultActiveKey={defaultActiveKey}
|
||||
activeKey={activeKey}
|
||||
destroyOnHidden={destroyInactivePanel}
|
||||
collapsible={collapsible}
|
||||
onChange={(keys) => {
|
||||
setActiveKeys(keys)
|
||||
onChange?.(keys)
|
||||
}}
|
||||
expandIcon={({ isActive }) => (
|
||||
<ChevronRight
|
||||
size={16}
|
||||
color="var(--color-text-3)"
|
||||
strokeWidth={1.5}
|
||||
style={{ transform: isActive ? 'rotate(90deg)' : 'rotate(0deg)' }}
|
||||
/>
|
||||
)}
|
||||
items={[
|
||||
{
|
||||
styles: collapseItemStyles,
|
||||
key: '1',
|
||||
label,
|
||||
extra,
|
||||
children
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(CustomCollapse)
|
||||
54
packages/ui/src/components/display/EmojiAvatar/index.tsx
Normal file
54
packages/ui/src/components/display/EmojiAvatar/index.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
// Original path: src/renderer/src/components/Avatar/EmojiAvatar.tsx
|
||||
import React, { memo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface EmojiAvatarProps {
|
||||
children: string
|
||||
size?: number
|
||||
fontSize?: number
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
const EmojiAvatar = ({
|
||||
ref,
|
||||
children,
|
||||
size = 31,
|
||||
fontSize,
|
||||
onClick,
|
||||
className,
|
||||
style
|
||||
}: EmojiAvatarProps & { ref?: React.RefObject<HTMLDivElement | null> }) => (
|
||||
<StyledEmojiAvatar
|
||||
ref={ref}
|
||||
$size={size}
|
||||
$fontSize={fontSize ?? size * 0.5}
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
style={style}>
|
||||
{children}
|
||||
</StyledEmojiAvatar>
|
||||
)
|
||||
|
||||
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)
|
||||
8
packages/ui/src/components/icons/OcrIcon/index.tsx
Normal file
8
packages/ui/src/components/icons/OcrIcon/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
// 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
|
||||
6
packages/ui/src/components/icons/ResetIcon/index.tsx
Normal file
6
packages/ui/src/components/icons/ResetIcon/index.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
// 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
|
||||
8
packages/ui/src/components/icons/ToolIcon/index.tsx
Normal file
8
packages/ui/src/components/icons/ToolIcon/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
// 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
|
||||
18
packages/ui/src/components/icons/UnWrapIcon/index.tsx
Normal file
18
packages/ui/src/components/icons/UnWrapIcon/index.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
// 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
|
||||
21
packages/ui/src/components/icons/WrapIcon/index.tsx
Normal file
21
packages/ui/src/components/icons/WrapIcon/index.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
// 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
|
||||
@ -1,5 +1,6 @@
|
||||
// Base Components
|
||||
export { default as CopyButton } from './base/CopyButton'
|
||||
export { default as CustomCollapse } from './base/CustomCollapse'
|
||||
export { default as CustomTag } from './base/CustomTag'
|
||||
export { default as DividerWithText } from './base/DividerWithText'
|
||||
export { default as EmojiIcon } from './base/EmojiIcon'
|
||||
@ -12,6 +13,7 @@ export { WarnTag } from './base/WarnTag'
|
||||
|
||||
// Display Components
|
||||
export { default as Ellipsis } from './display/Ellipsis'
|
||||
export { default as EmojiAvatar } from './display/EmojiAvatar'
|
||||
export { default as ExpandableText } from './display/ExpandableText'
|
||||
export { default as ThinkingEffect } from './display/ThinkingEffect'
|
||||
|
||||
@ -24,15 +26,23 @@ export { default as CopyIcon } from './icons/CopyIcon'
|
||||
export { default as DeleteIcon } from './icons/DeleteIcon'
|
||||
export { default as EditIcon } from './icons/EditIcon'
|
||||
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 { default as HelpTooltip } from './interactive/HelpTooltip'
|
||||
export { default as InfoTooltip } from './interactive/InfoTooltip'
|
||||
export { default as Selector } from './interactive/Selector'
|
||||
export { default as WarnTooltip } from './interactive/WarnTooltip'
|
||||
|
||||
// Composite Components (复合组件)
|
||||
// 暂无复合组件
|
||||
|
||||
21
packages/ui/src/components/interactive/HelpTooltip/index.tsx
Normal file
21
packages/ui/src/components/interactive/HelpTooltip/index.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
// Original path: src/renderer/src/components/TooltipIcons/HelpTooltip.tsx
|
||||
import { Tooltip, TooltipProps } from 'antd'
|
||||
import { HelpCircle } from 'lucide-react'
|
||||
|
||||
type InheritedTooltipProps = Omit<TooltipProps, 'children'>
|
||||
|
||||
interface HelpTooltipProps extends InheritedTooltipProps {
|
||||
iconColor?: string
|
||||
iconSize?: string | number
|
||||
iconStyle?: React.CSSProperties
|
||||
}
|
||||
|
||||
const HelpTooltip = ({ iconColor = 'var(--color-text-2)', iconSize = 14, iconStyle, ...rest }: HelpTooltipProps) => {
|
||||
return (
|
||||
<Tooltip {...rest}>
|
||||
<HelpCircle size={iconSize} color={iconColor} style={{ ...iconStyle }} role="img" aria-label="Help" />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default HelpTooltip
|
||||
197
packages/ui/src/components/interactive/Selector/index.tsx
Normal file
197
packages/ui/src/components/interactive/Selector/index.tsx
Normal file
@ -0,0 +1,197 @@
|
||||
// Original path: src/renderer/src/components/Selector.tsx
|
||||
import { Dropdown, DropdownProps } from 'antd'
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
interface SelectorOption<V = string | number> {
|
||||
label: string | ReactNode
|
||||
value: V
|
||||
type?: 'group'
|
||||
options?: SelectorOption<V>[]
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface BaseSelectorProps<V = string | number> {
|
||||
options: SelectorOption<V>[]
|
||||
placeholder?: string
|
||||
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
|
||||
/** 字体大小 */
|
||||
size?: number
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface SingleSelectorProps<V> extends BaseSelectorProps<V> {
|
||||
multiple?: false
|
||||
value?: V
|
||||
onChange: (value: V) => void
|
||||
}
|
||||
|
||||
interface MultipleSelectorProps<V> extends BaseSelectorProps<V> {
|
||||
multiple: true
|
||||
value?: V[]
|
||||
onChange: (value: V[]) => void
|
||||
}
|
||||
|
||||
type SelectorProps<V> = SingleSelectorProps<V> | MultipleSelectorProps<V>
|
||||
|
||||
const Selector = <V extends string | number>({
|
||||
options,
|
||||
value,
|
||||
onChange = () => {},
|
||||
placement = 'bottomRight',
|
||||
size = 13,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
multiple = false
|
||||
}: SelectorProps<V>) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const inputRef = useRef<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout
|
||||
if (open) {
|
||||
timer = setTimeout(() => {
|
||||
inputRef.current?.focus()
|
||||
}, 1)
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const selectedValues = useMemo(() => {
|
||||
if (multiple) {
|
||||
return (value as V[]) || []
|
||||
}
|
||||
return value !== undefined ? [value as V] : []
|
||||
}, [value, multiple])
|
||||
|
||||
const label = useMemo(() => {
|
||||
if (selectedValues.length > 0) {
|
||||
const findLabels = (opts: SelectorOption<V>[]): (string | ReactNode)[] => {
|
||||
const labels: (string | ReactNode)[] = []
|
||||
for (const opt of opts) {
|
||||
if (selectedValues.some((v) => v == opt.value)) {
|
||||
labels.push(opt.label)
|
||||
}
|
||||
if (opt.options) {
|
||||
labels.push(...findLabels(opt.options))
|
||||
}
|
||||
}
|
||||
return labels
|
||||
}
|
||||
const labels = findLabels(options)
|
||||
if (labels.length === 0) return placeholder
|
||||
if (labels.length === 1) return labels[0]
|
||||
return t('common.selectedItems', { count: labels.length })
|
||||
}
|
||||
return placeholder
|
||||
}, [selectedValues, placeholder, options, t])
|
||||
|
||||
const items = useMemo(() => {
|
||||
const mapOption = (option: SelectorOption<V>) => ({
|
||||
key: option.value,
|
||||
label: option.label,
|
||||
extra: <CheckIcon>{selectedValues.some((v) => v == option.value) && <Check size={14} />}</CheckIcon>,
|
||||
disabled: option.disabled,
|
||||
type: option.type || (option.options ? 'group' : undefined),
|
||||
children: option.options?.map(mapOption)
|
||||
})
|
||||
|
||||
return options.map(mapOption)
|
||||
}, [options, selectedValues])
|
||||
|
||||
function onClick(e: { key: string }) {
|
||||
if (disabled) return
|
||||
|
||||
const newValue = e.key as V
|
||||
if (multiple) {
|
||||
const newValues = selectedValues.includes(newValue)
|
||||
? selectedValues.filter((v) => v !== newValue)
|
||||
: [...selectedValues, newValue]
|
||||
;(onChange as MultipleSelectorProps<V>['onChange'])(newValues)
|
||||
} else {
|
||||
;(onChange as SingleSelectorProps<V>['onChange'])(newValue)
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenChange: DropdownProps['onOpenChange'] = (nextOpen, info) => {
|
||||
if (disabled) return
|
||||
|
||||
if (info.source === 'trigger' || nextOpen) {
|
||||
setOpen(nextOpen)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlayClassName="selector-dropdown"
|
||||
menu={{ items, onClick }}
|
||||
trigger={['click']}
|
||||
placement={placement}
|
||||
open={open && !disabled}
|
||||
onOpenChange={handleOpenChange}>
|
||||
<Label $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
|
||||
{label}
|
||||
<LabelIcon size={size + 3} />
|
||||
</Label>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
const LabelIcon = styled(ChevronsUpDown)`
|
||||
border-radius: 4px;
|
||||
padding: 2px 0;
|
||||
background-color: var(--color-background-soft);
|
||||
transition: background-color 0.2s;
|
||||
`
|
||||
|
||||
const Label = styled.div<{ $size: number; $open: boolean; $disabled: boolean; $isPlaceholder: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 99px;
|
||||
padding: 3px 2px 3px 10px;
|
||||
font-size: ${({ $size }) => $size}px;
|
||||
line-height: 1;
|
||||
cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
|
||||
opacity: ${({ $disabled }) => ($disabled ? 0.6 : 1)};
|
||||
color: ${({ $isPlaceholder }) => ($isPlaceholder ? 'var(--color-text-2)' : 'inherit')};
|
||||
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
opacity 0.2s;
|
||||
&:hover {
|
||||
${({ $disabled }) =>
|
||||
!$disabled &&
|
||||
css`
|
||||
background-color: var(--color-background-mute);
|
||||
${LabelIcon} {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`}
|
||||
}
|
||||
${({ $open, $disabled }) =>
|
||||
$open &&
|
||||
!$disabled &&
|
||||
css`
|
||||
background-color: var(--color-background-mute);
|
||||
${LabelIcon} {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const CheckIcon = styled.div`
|
||||
width: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
`
|
||||
|
||||
export default Selector
|
||||
26
packages/ui/src/components/interactive/WarnTooltip/index.tsx
Normal file
26
packages/ui/src/components/interactive/WarnTooltip/index.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
// Original path: src/renderer/src/components/TooltipIcons/WarnTooltip.tsx
|
||||
import { Tooltip, TooltipProps } from 'antd'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
|
||||
type InheritedTooltipProps = Omit<TooltipProps, 'children'>
|
||||
|
||||
interface WarnTooltipProps extends InheritedTooltipProps {
|
||||
iconColor?: string
|
||||
iconSize?: string | number
|
||||
iconStyle?: React.CSSProperties
|
||||
}
|
||||
|
||||
const WarnTooltip = ({
|
||||
iconColor = 'var(--color-status-warning)',
|
||||
iconSize = 14,
|
||||
iconStyle,
|
||||
...rest
|
||||
}: WarnTooltipProps) => {
|
||||
return (
|
||||
<Tooltip {...rest}>
|
||||
<AlertTriangle size={iconSize} color={iconColor} style={{ ...iconStyle }} role="img" aria-label="Information" />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default WarnTooltip
|
||||
Loading…
Reference in New Issue
Block a user