mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
💄 style(chat-input): rework Plus menu with toggle switches and grouped submenus (#15433)
This commit is contained in:
@@ -208,6 +208,7 @@
|
||||
"heteroAgent.cloudRepo.noRepos": "No repositories configured. Add them in agent settings.",
|
||||
"heteroAgent.cloudRepo.notSet": "No repo selected",
|
||||
"heteroAgent.cloudRepo.sectionTitle": "Repositories",
|
||||
"heteroAgent.executionTarget.downloadDesktop": "Get Desktop App",
|
||||
"heteroAgent.executionTarget.infoTooltip": "Pick a remote device to drive that machine from the web. \"This device\" runs the agent locally and is only available inside the desktop app.",
|
||||
"heteroAgent.executionTarget.loading": "Loading devices…",
|
||||
"heteroAgent.executionTarget.local": "This device",
|
||||
@@ -217,7 +218,6 @@
|
||||
"heteroAgent.executionTarget.online": "Online",
|
||||
"heteroAgent.executionTarget.sandbox": "Cloud sandbox",
|
||||
"heteroAgent.executionTarget.sandboxDesc": "Run in an ephemeral cloud sandbox",
|
||||
"heteroAgent.executionTarget.downloadDesktop": "Get Desktop App",
|
||||
"heteroAgent.executionTarget.title": "Execution Device",
|
||||
"heteroAgent.executionTarget.unknownDevice": "Unknown device",
|
||||
"heteroAgent.fullAccess.label": "Full access",
|
||||
@@ -412,6 +412,7 @@
|
||||
"platformAgent.deviceGuard.platformUnavailable.desc": "{{name}} is not installed on the connected device.",
|
||||
"platformAgent.deviceGuard.platformUnavailable.title": "{{name}} not available",
|
||||
"platformAgent.deviceGuard.refresh": "Refresh",
|
||||
"plus.addAttachments": "Attachments",
|
||||
"plus.addSkills": "Add Skills...",
|
||||
"plus.search.appSearch": "Smart Search",
|
||||
"plus.search.appSearchDesc": "LobeHub optimized search service, delivering best retrieval results.",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"actions.expand.on": "Expand",
|
||||
"actions.typobar.off": "Hide formatting toolbar",
|
||||
"actions.typobar.on": "Show formatting toolbar",
|
||||
"actions.typobar.title": "Formatting Tool",
|
||||
"autoSave.latest": "Latest version loaded",
|
||||
"autoSave.saved": "Saved",
|
||||
"autoSave.saving": "Auto-saving...",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"heteroAgent.cloudRepo.noRepos": "未配置仓库,请在助理设置中添加。",
|
||||
"heteroAgent.cloudRepo.notSet": "未选择仓库",
|
||||
"heteroAgent.cloudRepo.sectionTitle": "代码仓库",
|
||||
"heteroAgent.executionTarget.downloadDesktop": "下载桌面端",
|
||||
"heteroAgent.executionTarget.infoTooltip": "选择「远程设备」后可在网页中驱动该机器;「本机」仅在桌面端内运行 agent。",
|
||||
"heteroAgent.executionTarget.loading": "正在加载设备…",
|
||||
"heteroAgent.executionTarget.local": "本机",
|
||||
@@ -217,7 +218,6 @@
|
||||
"heteroAgent.executionTarget.online": "在线",
|
||||
"heteroAgent.executionTarget.sandbox": "云端沙箱",
|
||||
"heteroAgent.executionTarget.sandboxDesc": "在临时云端沙箱中运行",
|
||||
"heteroAgent.executionTarget.downloadDesktop": "下载桌面端",
|
||||
"heteroAgent.executionTarget.title": "执行设备",
|
||||
"heteroAgent.executionTarget.unknownDevice": "未知设备",
|
||||
"heteroAgent.fullAccess.label": "完全访问权限",
|
||||
@@ -412,6 +412,7 @@
|
||||
"platformAgent.deviceGuard.platformUnavailable.desc": "连接的设备上未安装 {{name}}。",
|
||||
"platformAgent.deviceGuard.platformUnavailable.title": "{{name}} 不可用",
|
||||
"platformAgent.deviceGuard.refresh": "刷新",
|
||||
"plus.addAttachments": "附件",
|
||||
"plus.addSkills": "添加技能...",
|
||||
"plus.search.appSearch": "智能检索",
|
||||
"plus.search.appSearchDesc": "LobeHub 专属调优的搜索服务,检索效果最佳。",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"actions.expand.on": "展开",
|
||||
"actions.typobar.off": "隐藏格式工具栏",
|
||||
"actions.typobar.on": "显示格式工具栏",
|
||||
"actions.typobar.title": "格式工具",
|
||||
"autoSave.latest": "已加载最新版本",
|
||||
"autoSave.saved": "已保存",
|
||||
"autoSave.saving": "自动保存中…",
|
||||
|
||||
@@ -14,7 +14,10 @@ import { agentByIdSelectors } from '@/store/agent/selectors';
|
||||
import { useAgentId } from '../../hooks/useAgentId';
|
||||
import CheckboxItem from '../components/CheckboxWithLoading';
|
||||
|
||||
const labelMaxWidth = 'min(400px, 56vw)';
|
||||
// Cap so the widest library/file row (icon + label + checkbox + paddings) stays within the
|
||||
// submenu's 320px footer-driven width, keeping it level with the skill submenu instead of
|
||||
// growing past it.
|
||||
const labelMaxWidth = 'min(210px, 45vw)';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
viewMore: css`
|
||||
@@ -24,9 +27,17 @@ const styles = createStaticStyles(({ css }) => ({
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
width: calc(100% + 8px);
|
||||
/* width 320 + margin-inline -12 anchors the submenu to 320px (matching the skill
|
||||
submenu) and lets the row span full width; padding-inline 12 lines its icon/text
|
||||
up with the menu items above. */
|
||||
width: 320px;
|
||||
min-height: 32px;
|
||||
margin-inline-start: -4px;
|
||||
|
||||
/* The footer wrapper adds padding-block: 8px top & bottom; the top keeps it separated
|
||||
from the list, but the bottom leaves a dead gap against the popup edge — cancel it. */
|
||||
margin-block-end: -8px;
|
||||
margin-inline: -12px;
|
||||
padding-inline: 12px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
|
||||
@@ -107,28 +118,11 @@ export const useControls = ({
|
||||
),
|
||||
}));
|
||||
|
||||
// Flat list (no "Libraries" / "Files" group headers): libraries first, then files.
|
||||
const relatedGroups: ItemType[] = [
|
||||
...(libraryItems.length > 0
|
||||
? [
|
||||
{
|
||||
children: libraryItems,
|
||||
key: 'relativeLibraries',
|
||||
label: t('knowledgeBase.libraries'),
|
||||
type: 'group' as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...libraryItems,
|
||||
...(libraryItems.length > 0 && fileItems.length > 0 ? [{ type: 'divider' as const }] : []),
|
||||
...(fileItems.length > 0
|
||||
? [
|
||||
{
|
||||
children: fileItems,
|
||||
key: 'relativeFiles',
|
||||
label: t('knowledgeBase.files'),
|
||||
type: 'group' as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...fileItems,
|
||||
];
|
||||
|
||||
const footer = (
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { validateVideoFileSize } from '@lobechat/utils/client';
|
||||
import type { IconProps } from '@lobehub/ui';
|
||||
import { Icon, Popover } from '@lobehub/ui';
|
||||
import { BrainOffIcon, GlobeOffIcon, SkillsIcon } from '@lobehub/ui/icons';
|
||||
import { GlobeOffIcon, SkillsIcon } from '@lobehub/ui/icons';
|
||||
import { Upload } from 'antd';
|
||||
import { css, cssVar, cx } from 'antd-style';
|
||||
import {
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
PlusIcon,
|
||||
SearchCheck,
|
||||
Settings2Icon,
|
||||
Store,
|
||||
TypeIcon,
|
||||
} from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -25,7 +24,6 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { message } from '@/components/AntdStaticMethods';
|
||||
import { openAttachKnowledgeModal } from '@/features/LibraryModal';
|
||||
import { createSkillStoreModal } from '@/features/SkillStore';
|
||||
import { useModelSupportToolUse } from '@/hooks/useModelSupportToolUse';
|
||||
import { useVisualMediaUploadAbility } from '@/hooks/useVisualMediaUploadAbility';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
@@ -235,7 +233,10 @@ const stripPopoverContent = (items?: ActionDropdownMenuItems): ActionDropdownMen
|
||||
delete nextItem.popoverContent;
|
||||
|
||||
if ('children' in nextItem && nextItem.children) {
|
||||
return { ...nextItem, children: stripPopoverContent(nextItem.children) };
|
||||
return {
|
||||
...nextItem,
|
||||
children: stripPopoverContent(nextItem.children),
|
||||
} as ActionDropdownMenuItems[number];
|
||||
}
|
||||
|
||||
if ('label' in nextItem) {
|
||||
@@ -315,9 +316,12 @@ const PlusAction = memo(() => {
|
||||
const activeSearchOption: 'off' | 'app' | 'provider' =
|
||||
searchMode === 'off' ? 'off' : useModelBuiltinSearch ? 'provider' : 'app';
|
||||
|
||||
const handleToggleMemory = useCallback(async () => {
|
||||
await updateAgentChatConfig({ memory: { enabled: !isMemoryEnabled } });
|
||||
}, [isMemoryEnabled, updateAgentChatConfig]);
|
||||
const handleToggleMemory = useCallback(
|
||||
async (enabled: boolean) => {
|
||||
await updateAgentChatConfig({ memory: { enabled } });
|
||||
},
|
||||
[updateAgentChatConfig],
|
||||
);
|
||||
|
||||
const handleSelectSearch = useCallback(
|
||||
async (option: 'off' | 'app' | 'provider') => {
|
||||
@@ -332,11 +336,6 @@ const PlusAction = memo(() => {
|
||||
[updateAgentChatConfig],
|
||||
);
|
||||
|
||||
const handleOpenTools = useCallback(() => {
|
||||
setDropdownOpen(false);
|
||||
createSkillStoreModal();
|
||||
}, []);
|
||||
|
||||
const handleToggleParams = useCallback(() => {
|
||||
setDropdownOpen(false);
|
||||
if (isParamsPanelActive) {
|
||||
@@ -399,7 +398,8 @@ const PlusAction = memo(() => {
|
||||
const uploadItems: ActionDropdownMenuItems = [
|
||||
{
|
||||
closeOnClick: false,
|
||||
icon: FileUp,
|
||||
// Match the 20px file/library icons below so the label lines up with those rows.
|
||||
icon: <Icon icon={FileUp} size={20} />,
|
||||
key: 'upload-file-or-image',
|
||||
label: (
|
||||
<Upload
|
||||
@@ -435,12 +435,11 @@ const PlusAction = memo(() => {
|
||||
const toolsItems: ActionDropdownMenuItems =
|
||||
isAgentModeEnabled && enableFC
|
||||
? [
|
||||
{ type: 'divider' },
|
||||
{
|
||||
children: skillMenuItems,
|
||||
footer: skillMarketFooter,
|
||||
header: skillMarketHeader,
|
||||
icon: activeIcon(SkillsIcon, activeSkillCount > 0),
|
||||
icon: SkillsIcon,
|
||||
key: 'tools',
|
||||
label: renderLabelWithCount(
|
||||
tSetting('tools.title'),
|
||||
@@ -452,38 +451,19 @@ const PlusAction = memo(() => {
|
||||
),
|
||||
),
|
||||
} as ActionDropdownMenuItems[number],
|
||||
{
|
||||
icon: Store,
|
||||
key: 'add-skills',
|
||||
label: t('plus.addSkills'),
|
||||
onClick: handleOpenTools,
|
||||
},
|
||||
{ type: 'divider' },
|
||||
]
|
||||
: [];
|
||||
|
||||
const capabilityItems: ActionDropdownMenuItems = [
|
||||
{ type: 'divider' },
|
||||
// Rich text toolbar toggle
|
||||
// Memory toggle — trailing switch; toggle by clicking the switch or the whole row
|
||||
{
|
||||
icon: TypeIcon,
|
||||
key: 'typo',
|
||||
label: renderActive(tEditor('actions.typobar.on'), Boolean(showTypoBar)),
|
||||
onClick: () => setShowTypoBar(!showTypoBar),
|
||||
},
|
||||
// Advanced parameter settings — mirrors ParamsPanelToggle in the agent header.
|
||||
{
|
||||
icon: Settings2Icon,
|
||||
key: 'params',
|
||||
label: renderActive(tSetting('settingModel.params.title'), isParamsPanelActive),
|
||||
onClick: handleToggleParams,
|
||||
},
|
||||
{ type: 'divider' },
|
||||
// Memory toggle
|
||||
{
|
||||
icon: activeIcon(isMemoryEnabled ? Brain : BrainOffIcon, Boolean(isMemoryEnabled)),
|
||||
checked: Boolean(isMemoryEnabled),
|
||||
icon: Brain,
|
||||
key: 'memory',
|
||||
label: renderActive(t('memory.title'), Boolean(isMemoryEnabled)),
|
||||
onClick: handleToggleMemory,
|
||||
label: t('memory.title'),
|
||||
onCheckedChange: handleToggleMemory,
|
||||
type: 'switch',
|
||||
},
|
||||
// Web search: simple toggle when 2 options, submenu when 3
|
||||
...(showProviderSearch
|
||||
@@ -538,32 +518,57 @@ const PlusAction = memo(() => {
|
||||
} as ActionDropdownMenuItems[number],
|
||||
]
|
||||
: [
|
||||
// Web search toggle — trailing switch; toggle by clicking the switch or the whole row
|
||||
{
|
||||
icon: activeIcon(
|
||||
activeSearchOption === 'off' ? GlobeOffIcon : Globe,
|
||||
activeSearchOption !== 'off',
|
||||
),
|
||||
checked: activeSearchOption !== 'off',
|
||||
icon: Globe,
|
||||
key: 'search-toggle',
|
||||
label: renderActive(t('search.title'), activeSearchOption !== 'off'),
|
||||
onClick: () => handleSelectSearch(activeSearchOption === 'off' ? 'app' : 'off'),
|
||||
label: t('search.title'),
|
||||
onCheckedChange: (checked: boolean) => handleSelectSearch(checked ? 'app' : 'off'),
|
||||
type: 'switch',
|
||||
} as ActionDropdownMenuItems[number],
|
||||
]),
|
||||
{ type: 'divider' },
|
||||
// Skills (with "Add Skills..." merged in) sits directly under the Web Search divider.
|
||||
...toolsItems,
|
||||
// Formatting toolbar toggle — trailing switch; toggle by clicking the switch or the whole row
|
||||
{
|
||||
checked: Boolean(showTypoBar),
|
||||
icon: TypeIcon,
|
||||
key: 'typo',
|
||||
label: tEditor('actions.typobar.title'),
|
||||
onCheckedChange: (checked: boolean) => setShowTypoBar(checked),
|
||||
type: 'switch',
|
||||
},
|
||||
// Advanced parameter settings — mirrors ParamsPanelToggle in the agent header.
|
||||
{
|
||||
icon: Settings2Icon,
|
||||
key: 'params',
|
||||
label: renderActive(tSetting('settingModel.params.title'), isParamsPanelActive),
|
||||
onClick: handleToggleParams,
|
||||
},
|
||||
];
|
||||
|
||||
const knowledgeSectionItems: ActionDropdownMenuItems = enableKnowledgeBase
|
||||
// "Add Attachments..." merges file upload with the knowledge base (libraries / files).
|
||||
// When the knowledge base is disabled there is no submenu, so Upload stays a top-level entry.
|
||||
const attachmentsItems: ActionDropdownMenuItems = enableKnowledgeBase
|
||||
? [
|
||||
{
|
||||
children: knowledgeItems,
|
||||
children: [
|
||||
...uploadItems,
|
||||
...(knowledgeItems.length > 0
|
||||
? [{ type: 'divider' as const }, ...knowledgeItems]
|
||||
: []),
|
||||
],
|
||||
footer: knowledgeFooter,
|
||||
icon: activeIcon(LibraryBig, knowledgeEnabledCount > 0),
|
||||
key: 'knowledge-base',
|
||||
label: renderLabelWithCount(t('knowledgeBase.title'), knowledgeEnabledCount),
|
||||
icon: LibraryBig,
|
||||
key: 'attachments',
|
||||
label: renderLabelWithCount(t('plus.addAttachments'), knowledgeEnabledCount),
|
||||
} as ActionDropdownMenuItems[number],
|
||||
]
|
||||
: [];
|
||||
: uploadItems;
|
||||
|
||||
return [...uploadItems, ...knowledgeSectionItems, ...capabilityItems];
|
||||
return [...attachmentsItems, ...capabilityItems];
|
||||
}, [
|
||||
activeSearchOption,
|
||||
canUploadImage,
|
||||
@@ -571,7 +576,6 @@ const PlusAction = memo(() => {
|
||||
editor,
|
||||
enableFC,
|
||||
enableKnowledgeBase,
|
||||
handleOpenTools,
|
||||
handleSelectSearch,
|
||||
handleToggleMemory,
|
||||
handleToggleParams,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Package,
|
||||
Pin,
|
||||
Settings,
|
||||
Store,
|
||||
Trash2,
|
||||
Wrench,
|
||||
Zap,
|
||||
@@ -30,6 +31,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import DevModal from '@/features/PluginDevModal';
|
||||
import { createSkillStoreModal } from '@/features/SkillStore';
|
||||
import { useCheckPluginsIsInstalled } from '@/hooks/useCheckPluginsIsInstalled';
|
||||
import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
@@ -347,7 +349,6 @@ const styles = createStaticStyles(({ css }) => ({
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-inline-start: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
@@ -1279,20 +1280,43 @@ export const useControls = ({ closeDropdown }: { closeDropdown?: () => void } =
|
||||
<Icon icon={Zap} size={12} />
|
||||
{allAutoItems.length}
|
||||
</span>
|
||||
<Tooltip placement="top" title={t('tools.plugins.management')}>
|
||||
<button
|
||||
aria-label={t('tools.plugins.management')}
|
||||
className={cx(styles.statsSettingsButton)}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
closeDropdown?.();
|
||||
navigate('/settings/skill');
|
||||
}}
|
||||
>
|
||||
<Icon icon={Settings} size={14} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<span
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: 'inline-flex',
|
||||
gap: 2,
|
||||
marginInlineStart: 'auto',
|
||||
}}
|
||||
>
|
||||
<Tooltip placement="top" title={t('plus.addSkills', { ns: 'chat' })}>
|
||||
<button
|
||||
aria-label={t('plus.addSkills', { ns: 'chat' })}
|
||||
className={cx(styles.statsSettingsButton)}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
closeDropdown?.();
|
||||
createSkillStoreModal();
|
||||
}}
|
||||
>
|
||||
<Icon icon={Store} size={14} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('tools.plugins.management')}>
|
||||
<button
|
||||
aria-label={t('tools.plugins.management')}
|
||||
className={cx(styles.statsSettingsButton)}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
closeDropdown?.();
|
||||
navigate('/settings/skill');
|
||||
}}
|
||||
>
|
||||
<Icon icon={Settings} size={14} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
) : undefined;
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ const hotArea = css`
|
||||
}
|
||||
`;
|
||||
|
||||
// Keep every row's leading icon the same width. The menu's icon slot sizes to its
|
||||
// content, so a larger file-type icon next to a smaller line icon would widen that
|
||||
// slot and push its label out of alignment with the upload / "view more" rows.
|
||||
const MENU_ICON_SIZE = 20;
|
||||
|
||||
const FileUpload = memo(() => {
|
||||
const { t } = useTranslation('chat');
|
||||
|
||||
@@ -76,7 +81,7 @@ const FileUpload = memo(() => {
|
||||
{
|
||||
closeOnClick: false,
|
||||
disabled: !canUploadImage,
|
||||
icon: ImageUp,
|
||||
icon: <Icon icon={ImageUp} size={MENU_ICON_SIZE} />,
|
||||
key: 'upload-image',
|
||||
label: canUploadImage ? (
|
||||
<Upload
|
||||
@@ -101,7 +106,7 @@ const FileUpload = memo(() => {
|
||||
},
|
||||
{
|
||||
closeOnClick: false,
|
||||
icon: FileUp,
|
||||
icon: <Icon icon={FileUp} size={MENU_ICON_SIZE} />,
|
||||
key: 'upload-file',
|
||||
label: (
|
||||
<Upload
|
||||
@@ -138,7 +143,7 @@ const FileUpload = memo(() => {
|
||||
},
|
||||
{
|
||||
closeOnClick: false,
|
||||
icon: FolderUp,
|
||||
icon: <Icon icon={FolderUp} size={MENU_ICON_SIZE} />,
|
||||
key: 'upload-folder',
|
||||
label: (
|
||||
<Upload
|
||||
@@ -184,7 +189,7 @@ const FileUpload = memo(() => {
|
||||
children: [
|
||||
// first the files
|
||||
...files.map((item) => ({
|
||||
icon: <FileIcon fileName={item.name} fileType={item.type} size={20} />,
|
||||
icon: <FileIcon fileName={item.name} fileType={item.type} size={MENU_ICON_SIZE} />,
|
||||
key: item.id,
|
||||
label: (
|
||||
<CheckboxItem
|
||||
@@ -202,7 +207,7 @@ const FileUpload = memo(() => {
|
||||
|
||||
// then the knowledge bases
|
||||
...knowledgeBases.map((item) => ({
|
||||
icon: <RepoIcon />,
|
||||
icon: <RepoIcon size={MENU_ICON_SIZE} />,
|
||||
key: item.id,
|
||||
label: (
|
||||
<CheckboxItem
|
||||
@@ -231,7 +236,7 @@ const FileUpload = memo(() => {
|
||||
},
|
||||
{
|
||||
extra: <Icon icon={ArrowRight} />,
|
||||
icon: LibraryBig,
|
||||
icon: <Icon icon={LibraryBig} size={MENU_ICON_SIZE} />,
|
||||
key: 'knowledge-base-store',
|
||||
label: t('knowledgeBase.viewMore'),
|
||||
onClick: () => {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
type BaseMenuItemType,
|
||||
type DropdownMenuPopupProps,
|
||||
type DropdownMenuProps,
|
||||
type MenuInfo,
|
||||
type MenuItemType,
|
||||
type MenuProps,
|
||||
type PopoverTrigger,
|
||||
@@ -95,7 +97,12 @@ const SubmenuScrollStyle = createGlobalStyle`
|
||||
|
||||
export type ActionDropdownMenuItem = MenuItemType;
|
||||
|
||||
export type ActionDropdownMenuItems = MenuProps<ActionDropdownMenuItem>['items'];
|
||||
/**
|
||||
* `renderDropdownMenuItems` accepts Base UI's full item union (`BaseMenuItemType`),
|
||||
* which is wider than antd's `MenuProps['items']` — it also covers `type: 'switch'`
|
||||
* and `type: 'checkbox'` items. Use it here so callers can declare those directly.
|
||||
*/
|
||||
export type ActionDropdownMenuItems = BaseMenuItemType[];
|
||||
|
||||
type ActionDropdownMenu = Omit<
|
||||
Pick<MenuProps<ActionDropdownMenuItem>, 'className' | 'onClick' | 'style'>,
|
||||
@@ -199,29 +206,40 @@ const ActionDropdown = memo<ActionDropdownProps>(
|
||||
children: item.children ? decorateMenuItems(item.children) : item.children,
|
||||
};
|
||||
}
|
||||
// Switch / checkbox items are self-contained: they toggle via `onCheckedChange`,
|
||||
// and Base UI already wires up "click the row or the control" for them. Pass them
|
||||
// through untouched instead of wrapping their click handler.
|
||||
if ('type' in item && (item.type === 'switch' || item.type === 'checkbox')) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if ('children' in item && item.children) {
|
||||
const originalOnOpenChange = (item as { onOpenChange?: unknown }).onOpenChange as
|
||||
| ((open: boolean, details: unknown) => void)
|
||||
| undefined;
|
||||
// Any item carrying a `children` key is a submenu (children may be optional);
|
||||
// route them all here so plain items never inherit a submenu's click signature.
|
||||
if ('children' in item) {
|
||||
return {
|
||||
...item,
|
||||
children: decorateMenuItems(item.children),
|
||||
children: item.children ? decorateMenuItems(item.children) : item.children,
|
||||
type: 'submenu',
|
||||
};
|
||||
// `children` is re-widened to the full item union; cast back to satisfy
|
||||
// the mixed rc-menu / Base UI submenu types in `BaseMenuItemType`.
|
||||
} as BaseMenuItemType;
|
||||
}
|
||||
const itemOnClick = 'onClick' in item ? item.onClick : undefined;
|
||||
const closeOnClick = 'closeOnClick' in item ? item.closeOnClick : undefined;
|
||||
// Submenus are handled above; everything else is a plain menu item. Base UI's
|
||||
// types keep optional-`children` submenu members in scope here, so narrow to the
|
||||
// plain-item type before wrapping the click handler.
|
||||
const menuItem = item as ActionDropdownMenuItem;
|
||||
const itemOnClick = menuItem.onClick;
|
||||
const closeOnClick = menuItem.closeOnClick;
|
||||
const keepOpenOnClick = closeOnClick === false;
|
||||
const itemLabel = 'label' in item ? item.label : undefined;
|
||||
const itemLabel = menuItem.label;
|
||||
const shouldKeepOpen = isValidElement(itemLabel);
|
||||
|
||||
const resolvedCloseOnClick = closeOnClick ?? (shouldKeepOpen ? false : undefined);
|
||||
|
||||
return {
|
||||
...item,
|
||||
...menuItem,
|
||||
...(resolvedCloseOnClick !== undefined ? { closeOnClick: resolvedCloseOnClick } : null),
|
||||
onClick: (info) => {
|
||||
onClick: (info: MenuInfo) => {
|
||||
if (keepOpenOnClick) {
|
||||
info.domEvent.stopPropagation();
|
||||
menu.onClick?.(info);
|
||||
|
||||
@@ -469,6 +469,7 @@ export default {
|
||||
'plus.search.modelSearchDesc': 'May cause unexpected behavior when enabled, not recommended.',
|
||||
'plus.search.off': 'Off',
|
||||
'plus.search.offDesc': '',
|
||||
'plus.addAttachments': 'Attachments',
|
||||
'plus.addSkills': 'Add Skills...',
|
||||
'plus.title': 'Add',
|
||||
'plus.tooltip': 'Add files, skills, and more context...',
|
||||
|
||||
@@ -3,6 +3,7 @@ export default {
|
||||
'actions.expand.on': 'Expand',
|
||||
'actions.typobar.off': 'Hide formatting toolbar',
|
||||
'actions.typobar.on': 'Show formatting toolbar',
|
||||
'actions.typobar.title': 'Formatting Tool',
|
||||
'autoSave.latest': 'Latest version loaded',
|
||||
'autoSave.saved': 'Saved',
|
||||
'autoSave.saving': 'Auto-saving...',
|
||||
|
||||
Reference in New Issue
Block a user