feat(ui): 添加工具栏溢出菜单优化响应式布局

- 新增 DropdownMenu 基础组件(基于 Radix UI)
- 新增 ToolbarOverflowMenu 组件(齿轮图标设置菜单)
- 将 Header 次要按钮收入溢出菜单
- 保留 Diagnostics 和 Sessions 按钮始终可见
- 溢出菜单放置在最右侧
This commit is contained in:
2025-12-17 17:52:20 +08:00
parent babe65719b
commit ddbd56a0ac
5 changed files with 167 additions and 78 deletions
@@ -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>
);
}
+1
View File
@@ -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;
+10
View File
@@ -32,3 +32,13 @@ export {
TooltipTrigger,
TooltipContent,
} from './Tooltip';
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuGroup,
DropdownMenuPortal,
} from './DropdownMenu';