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 { IDE } from './components/IDE.js';
|
||||||
export { StatusBar } from './components/StatusBar.js';
|
export { StatusBar } from './components/StatusBar.js';
|
||||||
export { Resizer } from './components/Resizer.js';
|
export { Resizer } from './components/Resizer.js';
|
||||||
|
export { ToolbarOverflowMenu, type ToolbarMenuItem } from './components/ToolbarOverflowMenu.js';
|
||||||
|
|
||||||
// Toast function (re-export from sonner)
|
// Toast function (re-export from sonner)
|
||||||
export { toast } 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,
|
TooltipTrigger,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
} from './Tooltip';
|
} from './Tooltip';
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
} from './DropdownMenu';
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
ContextUsage,
|
ContextUsage,
|
||||||
SubagentProgress,
|
SubagentProgress,
|
||||||
DiagnosticsIndicator,
|
DiagnosticsIndicator,
|
||||||
|
ToolbarOverflowMenu,
|
||||||
} from '@ai-assistant/ui';
|
} from '@ai-assistant/ui';
|
||||||
|
|
||||||
interface ChatPageProps {
|
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 按钮 */}
|
{/* Sessions 按钮 */}
|
||||||
{onOpenSessions && (
|
{onOpenSessions && (
|
||||||
<motion.button
|
<motion.button
|
||||||
@@ -247,6 +170,18 @@ export function ChatPage({
|
|||||||
<MessagesSquare size={20} />
|
<MessagesSquare size={20} />
|
||||||
</motion.button>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user