feat(ui): 添加工具栏溢出菜单优化响应式布局
- 新增 DropdownMenu 基础组件(基于 Radix UI) - 新增 ToolbarOverflowMenu 组件(齿轮图标设置菜单) - 将 Header 次要按钮收入溢出菜单 - 保留 Diagnostics 和 Sessions 按钮始终可见 - 溢出菜单放置在最右侧
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* ToolbarOverflowMenu Component
|
||||
*
|
||||
* 工具栏溢出菜单,用于在空间不足时收纳次要按钮
|
||||
*/
|
||||
|
||||
import { Settings, type LucideIcon } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '../primitives/DropdownMenu';
|
||||
import { cn } from '../utils/cn';
|
||||
|
||||
export interface ToolbarMenuItem {
|
||||
/** 菜单项图标 */
|
||||
icon: LucideIcon;
|
||||
/** 菜单项标签 */
|
||||
label: string;
|
||||
/** 点击回调 */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface ToolbarOverflowMenuProps {
|
||||
/** 菜单项列表 */
|
||||
items: ToolbarMenuItem[];
|
||||
/** 自定义类名 */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ToolbarOverflowMenu({ items, className }: ToolbarOverflowMenuProps) {
|
||||
// 过滤掉没有 onClick 的项
|
||||
const validItems = items.filter((item) => item.onClick);
|
||||
|
||||
if (validItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className={cn(
|
||||
'p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors',
|
||||
className
|
||||
)}
|
||||
title="Settings"
|
||||
>
|
||||
<Settings size={20} />
|
||||
</motion.button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{validItems.map((item) => (
|
||||
<DropdownMenuItem key={item.label} onClick={item.onClick}>
|
||||
<item.icon size={16} className="text-fg-muted" />
|
||||
<span>{item.label}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -255,6 +255,7 @@ export { CodeEditor, getLanguageFromFilename, type EditorTab } from './component
|
||||
export { IDE } from './components/IDE.js';
|
||||
export { StatusBar } from './components/StatusBar.js';
|
||||
export { Resizer } from './components/Resizer.js';
|
||||
export { ToolbarOverflowMenu, type ToolbarMenuItem } from './components/ToolbarOverflowMenu.js';
|
||||
|
||||
// Toast function (re-export from sonner)
|
||||
export { toast } from 'sonner';
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { forwardRef } from 'react';
|
||||
import { cn } from '../utils/cn';
|
||||
|
||||
export const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
export const DropdownMenuContent = forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-lg border border-line bg-surface-subtle p-1 text-fg shadow-md',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
|
||||
'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
export const DropdownMenuItem = forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors',
|
||||
'focus:bg-surface-muted focus:text-fg',
|
||||
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
export const DropdownMenuLabel = forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('px-2 py-1.5 text-sm font-semibold text-fg-muted', inset && 'pl-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
export const DropdownMenuSeparator = forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-line', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
@@ -32,3 +32,13 @@ export {
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
} from './Tooltip';
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
} from './DropdownMenu';
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ContextUsage,
|
||||
SubagentProgress,
|
||||
DiagnosticsIndicator,
|
||||
ToolbarOverflowMenu,
|
||||
} from '@ai-assistant/ui';
|
||||
|
||||
interface ChatPageProps {
|
||||
@@ -157,84 +158,6 @@ export function ChatPage({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Checkpoints 按钮 */}
|
||||
{onOpenCheckpoints && (
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenCheckpoints}
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Checkpoints"
|
||||
>
|
||||
<History size={20} />
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
{/* Providers 按钮 */}
|
||||
{onOpenProviders && (
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenProviders}
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Model Providers"
|
||||
>
|
||||
<Server size={20} />
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
{/* Agents 按钮 */}
|
||||
{onOpenAgents && (
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenAgents}
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Agent Presets"
|
||||
>
|
||||
<Bot size={20} />
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
{/* Hooks 按钮 */}
|
||||
{onOpenHooks && (
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenHooks}
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Hooks"
|
||||
>
|
||||
<Zap size={20} />
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
{/* MCP 按钮 */}
|
||||
{onOpenMCP && (
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenMCP}
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="MCP Servers"
|
||||
>
|
||||
<Plug size={20} />
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
{/* 命令按钮 */}
|
||||
{onOpenCommands && (
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onOpenCommands}
|
||||
className="p-1.5 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-muted transition-colors"
|
||||
title="Commands"
|
||||
>
|
||||
<Terminal size={20} />
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
{/* Sessions 按钮 */}
|
||||
{onOpenSessions && (
|
||||
<motion.button
|
||||
@@ -247,6 +170,18 @@ export function ChatPage({
|
||||
<MessagesSquare size={20} />
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
{/* 设置菜单 - 齿轮图标,放在最右侧 */}
|
||||
<ToolbarOverflowMenu
|
||||
items={[
|
||||
{ icon: History, label: 'Checkpoints', onClick: onOpenCheckpoints },
|
||||
{ icon: Server, label: 'Model Providers', onClick: onOpenProviders },
|
||||
{ icon: Bot, label: 'Agent Presets', onClick: onOpenAgents },
|
||||
{ icon: Zap, label: 'Hooks', onClick: onOpenHooks },
|
||||
{ icon: Plug, label: 'MCP Servers', onClick: onOpenMCP },
|
||||
{ icon: Terminal, label: 'Commands', onClick: onOpenCommands },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user