From db4fcac7680a9f7b8fbaa61e4a40857fab6d60ca Mon Sep 17 00:00:00 2001 From: MyPrototypeWhat Date: Tue, 30 Sep 2025 14:59:33 +0800 Subject: [PATCH] feat: enhance Selector component with SearchableSelector and update exports - Introduced a new SearchableSelector component for improved item selection with search functionality. - Updated the Selector component to streamline item selection and added type exports for better type safety. - Refactored the preferenceSchemas to use the new MathEngine type for better clarity. - Added comprehensive README documentation for the Selector component detailing usage and features. - Updated various components and stories to utilize the new Selector and SearchableSelector components. --- .../data/preference/preferenceSchemas.ts | 2 +- .../shared/data/preference/preferenceTypes.ts | 2 + .../ui/src/components/base/Selector/README.md | 333 ++++++++++++++++++ .../base/Selector/SearchableSelector.tsx | 60 ++++ .../src/components/base/Selector/Selector.tsx | 75 ++++ .../ui/src/components/base/Selector/index.tsx | 64 +--- .../ui/src/components/base/Selector/types.ts | 79 +++++ packages/ui/src/components/index.ts | 26 +- .../interactive/Selector.stories.tsx | 72 ++-- .../src/pages/home/Tabs/SettingsTab.tsx | 137 ++++--- 10 files changed, 711 insertions(+), 139 deletions(-) create mode 100644 packages/ui/src/components/base/Selector/README.md create mode 100644 packages/ui/src/components/base/Selector/SearchableSelector.tsx create mode 100644 packages/ui/src/components/base/Selector/Selector.tsx create mode 100644 packages/ui/src/components/base/Selector/types.ts diff --git a/packages/shared/data/preference/preferenceSchemas.ts b/packages/shared/data/preference/preferenceSchemas.ts index eea59b180d..4a37cde378 100644 --- a/packages/shared/data/preference/preferenceSchemas.ts +++ b/packages/shared/data/preference/preferenceSchemas.ts @@ -130,7 +130,7 @@ export interface PreferenceSchemas { // redux/settings/fontSize 'chat.message.font_size': number // redux/settings/mathEngine - 'chat.message.math.engine': string + 'chat.message.math.engine': PreferenceTypes.MathEngine // redux/settings/mathEnableSingleDollar 'chat.message.math.single_dollar': boolean // redux/settings/foldDisplayMode diff --git a/packages/shared/data/preference/preferenceTypes.ts b/packages/shared/data/preference/preferenceTypes.ts index 44f3d448f6..7a9af886c6 100644 --- a/packages/shared/data/preference/preferenceTypes.ts +++ b/packages/shared/data/preference/preferenceTypes.ts @@ -70,6 +70,8 @@ export type ProxyMode = 'system' | 'custom' | 'none' export type MultiModelFoldDisplayMode = 'expanded' | 'compact' +export type MathEngine = 'KaTeX' | 'MathJax' | 'none' + export enum UpgradeChannel { LATEST = 'latest', // 最新稳定版本 RC = 'rc', // 公测版本 diff --git a/packages/ui/src/components/base/Selector/README.md b/packages/ui/src/components/base/Selector/README.md new file mode 100644 index 0000000000..003a6f683e --- /dev/null +++ b/packages/ui/src/components/base/Selector/README.md @@ -0,0 +1,333 @@ +# Selector 组件 + +基于 HeroUI Select 封装的下拉选择组件,简化了 Set 和 Selection 的转换逻辑。 + +## 核心特性 + +- ✅ **类型安全**: 单选和多选自动推断回调类型 +- ✅ **智能转换**: 自动处理 `Set` 和原始值的转换 +- ✅ **HeroUI 风格**: 保持与 HeroUI 生态一致的 API +- ✅ **支持数字和字符串**: 泛型支持,自动识别值类型 + +## 基础用法 + +### 单选模式(默认) + +```tsx +import { Selector } from '@cherrystudio/ui' +import { useState } from 'react' + +function Example() { + const [language, setLanguage] = useState('zh-CN') + + const languageOptions = [ + { label: '中文', value: 'zh-CN' }, + { label: 'English', value: 'en-US' }, + { label: '日本語', value: 'ja-JP' } + ] + + return ( + { + // value 类型自动推断为 string + setLanguage(value) + }} + items={languageOptions} + placeholder="选择语言" + /> + ) +} +``` + +### 多选模式 + +```tsx +import { Selector } from '@cherrystudio/ui' +import { useState } from 'react' + +function Example() { + const [languages, setLanguages] = useState(['zh-CN', 'en-US']) + + const languageOptions = [ + { label: '中文', value: 'zh-CN' }, + { label: 'English', value: 'en-US' }, + { label: '日本語', value: 'ja-JP' }, + { label: 'Français', value: 'fr-FR' } + ] + + return ( + { + // values 类型自动推断为 string[] + setLanguages(values) + }} + items={languageOptions} + placeholder="选择语言" + /> + ) +} +``` + +### 数字类型值 + +```tsx +import { Selector } from '@cherrystudio/ui' + +function Example() { + const [priority, setPriority] = useState(1) + + const priorityOptions = [ + { label: '低', value: 1 }, + { label: '中', value: 2 }, + { label: '高', value: 3 } + ] + + return ( + + selectedKeys={priority} + onSelectionChange={(value) => { + // value 类型为 number + setPriority(value) + }} + items={priorityOptions} + /> + ) +} +``` + +### 禁用选项 + +```tsx +const options = [ + { label: '选项 1', value: '1' }, + { label: '选项 2 (禁用)', value: '2', disabled: true }, + { label: '选项 3', value: '3' } +] + + +``` + +### 自定义 Label + +```tsx +import { Flex } from '@cherrystudio/ui' + +const options = [ + { + label: ( + + 🇨🇳 + 中文 + + ), + value: 'zh-CN' + }, + { + label: ( + + 🇺🇸 + English + + ), + value: 'en-US' + } +] + + +``` + +## API + +### SelectorProps + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `items` | `SelectorItem[]` | - | 必填,选项列表 | +| `selectedKeys` | `V` \| `V[]` | - | 受控的选中值(单选为单个值,多选为数组) | +| `onSelectionChange` | `(key: V) => void` \| `(keys: V[]) => void` | - | 选择变化回调(类型根据 selectionMode 自动推断) | +| `selectionMode` | `'single'` \| `'multiple'` | `'single'` | 选择模式 | +| `placeholder` | `string` | - | 占位文本 | +| `disabled` | `boolean` | `false` | 是否禁用 | +| `isRequired` | `boolean` | `false` | 是否必填 | +| `label` | `ReactNode` | - | 标签文本 | +| `description` | `ReactNode` | - | 描述文本 | +| `errorMessage` | `ReactNode` | - | 错误提示 | +| ...rest | `SelectProps` | - | 其他 HeroUI Select 属性 | + +### SelectorItem + +```tsx +interface SelectorItem { + label: string | ReactNode // 显示文本或自定义内容 + value: V // 选项值 + disabled?: boolean // 是否禁用 + [key: string]: any // 其他自定义属性 +} +``` + +## 类型安全 + +组件使用 TypeScript 条件类型,根据 `selectionMode` 自动推断回调类型: + +```tsx +// 单选模式 + ...} // v 类型: V +/> + +// 多选模式 + ...} // vs 类型: V[] +/> +``` + +## 与 HeroUI Select 的区别 + +| 特性 | HeroUI Select | Selector (本组件) | +|------|---------------|------------------| +| `selectedKeys` | `Set \| 'all'` | `V` \| `V[]` (自动转换) | +| `onSelectionChange` | `(keys: Selection) => void` | `(key: V) => void` \| `(keys: V[]) => void` | +| 单选回调 | 返回 `Set` (需手动提取) | 直接返回单个值 | +| 多选回调 | 返回 `Set` (需转数组) | 直接返回数组 | +| 类型推断 | 无 | 根据 selectionMode 自动推断 | + +## 最佳实践 + +### 1. 显式声明 selectionMode + +虽然单选是默认模式,但建议显式声明以提高代码可读性: + +```tsx +// ✅ 推荐 + + +// ⚠️ 可以但不够清晰 + +``` + +### 2. 使用泛型指定值类型 + +当值类型为数字或联合类型时,使用泛型获得更好的类型提示: + +```tsx +// ✅ 推荐 + selectedKeys={priority} ... /> + +// ✅ 推荐(联合类型) +type Status = 'pending' | 'approved' | 'rejected' + selectedKeys={status} ... /> +``` + +### 3. 避免在渲染时创建 items + +```tsx +// ❌ 不推荐(每次渲染都创建新数组) + + +// ✅ 推荐(在组件外或使用 useMemo) +const items = [{ label: 'A', value: '1' }] + +``` + +## 迁移指南 + +### 从 antd Select 迁移 + +```tsx +// antd Select +import { Select } from 'antd' + + | undefined} + onSelectionChange={handleSelectionChange}> + {children ?? defaultRenderItem} + + ) +} + +export default Selector diff --git a/packages/ui/src/components/base/Selector/index.tsx b/packages/ui/src/components/base/Selector/index.tsx index eeb7ab7759..2a1b3359bc 100644 --- a/packages/ui/src/components/base/Selector/index.tsx +++ b/packages/ui/src/components/base/Selector/index.tsx @@ -1,51 +1,13 @@ -import type { Selection, SelectProps } from '@heroui/react' -import { Select, SelectItem } from '@heroui/react' -import type { ReactNode } from 'react' - -interface SelectorItem { - label: string | ReactNode - value: V - disabled?: boolean - [key: string]: any -} - -interface SelectorProps extends Omit { - items: SelectorItem[] - onSelectionChange?: (keys: V[]) => void -} - -const Selector = ({ items, onSelectionChange, ...rest }: SelectorProps) => { - // 处理选择变化,转换 Set 为数组 - const handleSelectionChange = (keys: Selection) => { - if (!onSelectionChange) return - if (keys === 'all') { - // 如果是全选,返回所有非禁用项的值 - const allValues = items.filter((item) => !item.disabled).map((item) => item.value) - onSelectionChange(allValues) - return - } - - // 转换 Set 为原始类型数组 - const keysArray = Array.from(keys).map((key) => { - const strKey = String(key) - // 尝试转换回数字类型(如果原始值是数字) - const num = Number(strKey) - return !isNaN(num) && items.some((item) => item.value === num) ? num : strKey - }) as V[] - - onSelectionChange(keysArray) - } - - return ( - - ) -} - -export default Selector -export type { SelectorItem, SelectorProps } +// 统一导出 Selector 相关组件和类型 +export { default as SearchableSelector } from './SearchableSelector' +export { default } from './Selector' +export type { + MultipleSearchableSelectorProps, + MultipleSelectorProps, + SearchableSelectorItem, + SearchableSelectorProps, + SelectorItem, + SelectorProps, + SingleSearchableSelectorProps, + SingleSelectorProps +} from './types' diff --git a/packages/ui/src/components/base/Selector/types.ts b/packages/ui/src/components/base/Selector/types.ts new file mode 100644 index 0000000000..c8295d5731 --- /dev/null +++ b/packages/ui/src/components/base/Selector/types.ts @@ -0,0 +1,79 @@ +import type { AutocompleteProps, SelectProps } from '@heroui/react' +import type { ReactElement, ReactNode } from 'react' + +interface SelectorItem { + label?: string | ReactNode + value: V + disabled?: boolean + [key: string]: any +} + +// 自定义渲染函数类型 +type SelectorRenderItem = (item: T) => ReactElement + +// 单选模式的 Props +interface SingleSelectorProps + extends Omit, 'children' | 'onSelectionChange' | 'selectedKeys' | 'selectionMode'> { + items: T[] + selectionMode?: 'single' + selectedKeys?: T['value'] + onSelectionChange?: (key: T['value']) => void + children?: SelectorRenderItem +} + +// 多选模式的 Props +interface MultipleSelectorProps + extends Omit, 'children' | 'onSelectionChange' | 'selectedKeys' | 'selectionMode'> { + items: T[] + selectionMode: 'multiple' + selectedKeys?: T['value'][] + onSelectionChange?: (keys: T['value'][]) => void + children?: SelectorRenderItem +} + +type SelectorProps = SingleSelectorProps | MultipleSelectorProps + +interface SearchableSelectorItem { + label?: string | ReactNode + value: V + disabled?: boolean + [key: string]: any +} + +// 自定义渲染函数类型 +type SearchableRenderItem = (item: T) => ReactElement + +// 单选模式的 Props +interface SingleSearchableSelectorProps + extends Omit, 'children' | 'onSelectionChange' | 'selectedKey' | 'selectionMode'> { + items: T[] + selectionMode?: 'single' + selectedKeys?: T['value'] + onSelectionChange?: (key: T['value']) => void + children?: SearchableRenderItem +} + +// 多选模式的 Props +interface MultipleSearchableSelectorProps + extends Omit, 'children' | 'onSelectionChange' | 'selectedKey' | 'selectionMode'> { + items: T[] + selectionMode: 'multiple' + selectedKeys?: T['value'][] + onSelectionChange?: (keys: T['value'][]) => void + children?: SearchableRenderItem +} + +type SearchableSelectorProps = + | SingleSearchableSelectorProps + | MultipleSearchableSelectorProps + +export type { + MultipleSearchableSelectorProps, + MultipleSelectorProps, + SearchableSelectorItem, + SearchableSelectorProps, + SelectorItem, + SelectorProps, + SingleSearchableSelectorProps, + SingleSelectorProps +} diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index c85d8d6bea..39a2a6d4c7 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -1,5 +1,5 @@ // Base Components -export { Avatar, AvatarGroup, type AvatarProps,EmojiAvatar } from './base/Avatar' +export { Avatar, AvatarGroup, type AvatarProps, EmojiAvatar } from './base/Avatar' export { default as Button, type ButtonProps } from './base/Button' export { default as CopyButton } from './base/CopyButton' export { default as CustomCollapse } from './base/CustomCollapse' @@ -48,8 +48,22 @@ export { export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring' export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon' -// Interactive Components +/* Interactive Components */ + +// Selector / SearchableSelector export { default as Selector } from './base/Selector' +export { default as SearchableSelector } from './base/Selector/SearchableSelector' +export type { + MultipleSearchableSelectorProps, + MultipleSelectorProps, + SearchableSelectorItem, + SearchableSelectorProps, + SelectorItem, + SelectorProps, + SingleSearchableSelectorProps, + SingleSelectorProps +} from './base/Selector/types' +// CodeEditor export { default as CodeEditor, type CodeEditorHandles, @@ -58,14 +72,22 @@ export { getCmThemeByName, getCmThemeNames } from './interactive/CodeEditor' +// CollapsibleSearchBar export { default as CollapsibleSearchBar } from './interactive/CollapsibleSearchBar' +// DraggableList export { DraggableList, useDraggableReorder } from './interactive/DraggableList' +// EditableNumber export type { EditableNumberProps } from './interactive/EditableNumber' +// EditableNumber export { default as EditableNumber } from './interactive/EditableNumber' export { default as HelpTooltip } from './interactive/HelpTooltip' +// ImageToolButton export { default as ImageToolButton } from './interactive/ImageToolButton' +// InfoTooltip export { default as InfoTooltip } from './interactive/InfoTooltip' +// Sortable export { Sortable } from './interactive/Sortable' +// WarnTooltip export { default as WarnTooltip } from './interactive/WarnTooltip' // Composite Components (复合组件) diff --git a/packages/ui/stories/components/interactive/Selector.stories.tsx b/packages/ui/stories/components/interactive/Selector.stories.tsx index 49f4d8f4c4..15f3e0925d 100644 --- a/packages/ui/stories/components/interactive/Selector.stories.tsx +++ b/packages/ui/stories/components/interactive/Selector.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react' +import type { Meta } from '@storybook/react' import { useState } from 'react' import Selector from '../../../src/components/base/Selector' @@ -49,18 +49,18 @@ const meta: Meta = { } export default meta -type Story = StoryObj -// 基础用法 -export const Default: Story = { +// 基础用法 - 单选 +export const Default = { render: function Render() { - const [selectedKeys, setSelectedKeys] = useState>(new Set(['react'])) + const [selectedValue, setSelectedValue] = useState('react') return (
setSelectedKeys(new Set(keys.map(String)))} + selectionMode="single" + selectedKeys={selectedValue} + onSelectionChange={(value) => setSelectedValue(value)} placeholder="选择框架" items={[ { value: 'react', label: 'React' }, @@ -70,7 +70,7 @@ export const Default: Story = { ]} />
- 当前选择: {Array.from(selectedKeys).join(', ')} + 当前选择: {selectedValue}
) @@ -78,16 +78,16 @@ export const Default: Story = { } // 多选模式 -export const Multiple: Story = { +export const Multiple = { render: function Render() { - const [selectedKeys, setSelectedKeys] = useState>(new Set(['react', 'vue'])) + const [selectedValues, setSelectedValues] = useState(['react', 'vue']) return (
setSelectedKeys(new Set(keys.map(String)))} + selectedKeys={selectedValues} + onSelectionChange={(values) => setSelectedValues(values)} placeholder="选择多个框架" items={[ { value: 'react', label: 'React' }, @@ -98,7 +98,7 @@ export const Multiple: Story = { ]} />
- 已选择 ({selectedKeys.size}): {Array.from(selectedKeys).join(', ')} + 已选择 ({selectedValues.length}): {selectedValues.join(', ')}
) @@ -106,19 +106,16 @@ export const Multiple: Story = { } // 数字值类型 -export const NumberValues: Story = { +export const NumberValues = { render: function Render() { - const [selectedKeys, setSelectedKeys] = useState>(new Set(['2'])) const [selectedValue, setSelectedValue] = useState(2) return (
{ - setSelectedKeys(new Set(keys.map(String))) - setSelectedValue(keys[0] as number) - }} + selectionMode="single" + selectedKeys={selectedValue} + onSelectionChange={(value) => setSelectedValue(value)} placeholder="选择优先级" items={[ { value: 1, label: '🔴 紧急' }, @@ -136,7 +133,7 @@ export const NumberValues: Story = { } // 不同大小 -export const Sizes: Story = { +export const Sizes = { render: function Render() { const items = [ { value: 'option1', label: '选项 1' }, @@ -164,22 +161,26 @@ export const Sizes: Story = { } // 禁用状态 -export const Disabled: Story = { - args: { - isDisabled: true, - selectedKeys: new Set(['react']), - placeholder: '禁用的选择器', - items: [ - { value: 'react', label: 'React' }, - { value: 'vue', label: 'Vue' } - ] +export const Disabled = { + render: function Render() { + return ( + + ) } } // 实际应用场景:语言选择 -export const LanguageSelector: Story = { +export const LanguageSelector = { render: function Render() { - const [selectedKeys, setSelectedKeys] = useState>(new Set(['zh'])) + const [selectedValue, setSelectedValue] = useState('zh') const languages = [ { value: 'zh', label: '🇨🇳 简体中文' }, @@ -193,13 +194,14 @@ export const LanguageSelector: Story = { return (
setSelectedKeys(new Set(keys.map(String)))} + selectionMode="single" + selectedKeys={selectedValue} + onSelectionChange={(value) => setSelectedValue(value)} placeholder="选择语言" items={languages} />
- 当前语言: {languages.find((l) => l.value === Array.from(selectedKeys)[0])?.label} + 当前语言: {languages.find((l) => l.value === selectedValue)?.label}
) diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index efeb69c92c..456ea3ac1c 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -1,4 +1,4 @@ -import { Button, DescriptionSwitch, RowFlex, Selector } from '@cherrystudio/ui' +import { Button, DescriptionSwitch, RowFlex, Selector, type SelectorItem } from '@cherrystudio/ui' import { useMultiplePreferences, usePreference } from '@data/hooks/usePreference' import EditableNumber from '@renderer/components/EditableNumber' import Scrollbar from '@renderer/components/Scrollbar' @@ -18,7 +18,7 @@ import { getDefaultModel } from '@renderer/services/AssistantService' import type { Assistant, AssistantSettings, CodeStyleVarious, MathEngine } from '@renderer/types' import { modalConfirm } from '@renderer/utils' import { getSendMessageShortcutLabel } from '@renderer/utils/input' -import type { SendMessageShortcut } from '@shared/data/preference/preferenceTypes' +import type { MultiModelMessageStyle, SendMessageShortcut } from '@shared/data/preference/preferenceTypes' import { ThemeMode } from '@shared/data/preference/preferenceTypes' import { Col, InputNumber, Row, Slider } from 'antd' import { Settings2 } from 'lucide-react' @@ -101,6 +101,63 @@ const SettingsTab: FC = (props) => { const { t } = useTranslation() + const messageStyleItems = useMemo[]>( + () => [ + { value: 'plain', label: t('message.message.style.plain') }, + { value: 'bubble', label: t('message.message.style.bubble') } + ], + [t] + ) + + const multiModelMessageStyleItems = useMemo[]>( + () => [ + { value: 'fold', label: t('message.message.multi_model_style.fold.label') }, + { value: 'vertical', label: t('message.message.multi_model_style.vertical') }, + { value: 'horizontal', label: t('message.message.multi_model_style.horizontal') }, + { value: 'grid', label: t('message.message.multi_model_style.grid') } + ], + [t] + ) + + const messageNavigationItems = useMemo[]>( + () => [ + { value: 'none', label: t('settings.messages.navigation.none') }, + { value: 'buttons', label: t('settings.messages.navigation.buttons') }, + { value: 'anchor', label: t('settings.messages.navigation.anchor') } + ], + [t] + ) + + const mathEngineItems = useMemo[]>( + () => [ + { value: 'KaTeX', label: 'KaTeX' }, + { value: 'MathJax', label: 'MathJax' }, + { value: 'none', label: t('settings.math.engine.none') } + ], + [t] + ) + + const codeStyleItems = useMemo[]>( + () => themeNames.map((theme) => ({ value: theme, label: theme })), + [themeNames] + ) + + const targetLanguageItems = useMemo[]>( + () => translateLanguages.map((item) => ({ value: item.langCode, label: item.emoji + ' ' + item.label() })), + [translateLanguages] + ) + + const sendMessageShortcutItems = useMemo[]>( + () => [ + { value: 'Enter', label: getSendMessageShortcutLabel('Enter') }, + { value: 'Ctrl+Enter', label: getSendMessageShortcutLabel('Ctrl+Enter') }, + { value: 'Alt+Enter', label: getSendMessageShortcutLabel('Alt+Enter') }, + { value: 'Command+Enter', label: getSendMessageShortcutLabel('Command+Enter') }, + { value: 'Shift+Enter', label: getSendMessageShortcutLabel('Shift+Enter') } + ], + [] + ) + const onUpdateAssistantSettings = (settings: Partial) => { updateAssistantSettings(settings) } @@ -342,12 +399,10 @@ const SettingsTab: FC = (props) => { setMessageStyle(value[0] as 'plain' | 'bubble')} - items={[ - { value: 'plain', label: t('message.message.style.plain') }, - { value: 'bubble', label: t('message.message.style.bubble') } - ]} + selectionMode="single" + selectedKeys={messageStyle} + onSelectionChange={(value) => setMessageStyle(value)} + items={messageStyleItems} /> @@ -356,14 +411,10 @@ const SettingsTab: FC = (props) => { setMultiModelMessageStyle(value[0])} - items={[ - { value: 'fold', label: t('message.message.multi_model_style.fold.label') }, - { value: 'vertical', label: t('message.message.multi_model_style.vertical') }, - { value: 'horizontal', label: t('message.message.multi_model_style.horizontal') }, - { value: 'grid', label: t('message.message.multi_model_style.grid') } - ]} + selectionMode="single" + selectedKeys={multiModelMessageStyle} + onSelectionChange={(value) => setMultiModelMessageStyle(value)} + items={multiModelMessageStyleItems} /> @@ -372,13 +423,10 @@ const SettingsTab: FC = (props) => { setMessageNavigation(value[0] as 'none' | 'buttons' | 'anchor')} - items={[ - { value: 'none', label: t('settings.messages.navigation.none') }, - { value: 'buttons', label: t('settings.messages.navigation.buttons') }, - { value: 'anchor', label: t('settings.messages.navigation.anchor') } - ]} + selectionMode="single" + selectedKeys={messageNavigation} + onSelectionChange={(value) => setMessageNavigation(value)} + items={messageNavigationItems} /> @@ -412,13 +460,10 @@ const SettingsTab: FC = (props) => { setMathEngine(value[0] as MathEngine)} - items={[ - { value: 'KaTeX', label: 'KaTeX' }, - { value: 'MathJax', label: 'MathJax' }, - { value: 'none', label: t('settings.math.engine.none') } - ]} + selectionMode="single" + selectedKeys={mathEngine} + onSelectionChange={(value) => setMathEngine(value)} + items={mathEngineItems} /> @@ -444,12 +489,10 @@ const SettingsTab: FC = (props) => { onCodeStyleChange(value[0] as CodeStyleVarious)} - items={themeNames.map((theme) => ({ - value: theme, - label: theme - }))} + selectionMode="single" + selectedKeys={codeStyle} + onSelectionChange={(value) => onCodeStyleChange(value)} + items={codeStyleItems} /> @@ -682,12 +725,11 @@ const SettingsTab: FC = (props) => { setTargetLanguage(value[0])} + selectionMode="single" + selectedKeys={targetLanguage} + onSelectionChange={(value) => setTargetLanguage(value)} placeholder={UNKNOWN.emoji + ' ' + UNKNOWN.label()} - items={translateLanguages.map((item) => { - return { value: item.langCode, label: item.emoji + ' ' + item.label() } - })} + items={targetLanguageItems} /> @@ -696,15 +738,10 @@ const SettingsTab: FC = (props) => { setSendMessageShortcut(value[0] as SendMessageShortcut)} - items={[ - { value: 'Enter', label: getSendMessageShortcutLabel('Enter') }, - { value: 'Ctrl+Enter', label: getSendMessageShortcutLabel('Ctrl+Enter') }, - { value: 'Alt+Enter', label: getSendMessageShortcutLabel('Alt+Enter') }, - { value: 'Command+Enter', label: getSendMessageShortcutLabel('Command+Enter') }, - { value: 'Shift+Enter', label: getSendMessageShortcutLabel('Shift+Enter') } - ]} + selectionMode="single" + selectedKeys={sendMessageShortcut} + onSelectionChange={(value) => setSendMessageShortcut(value)} + items={sendMessageShortcutItems} />