diff --git a/apps/desktop/src/renderer/src/components/SidePanel.tsx b/apps/desktop/src/renderer/src/components/SidePanel.tsx index 8be03ff..043ac04 100644 --- a/apps/desktop/src/renderer/src/components/SidePanel.tsx +++ b/apps/desktop/src/renderer/src/components/SidePanel.tsx @@ -3,8 +3,9 @@ import { LuChevronDown, LuPlus, LuX } from 'react-icons/lu' /** * 右侧通用标签面板(TabPanel,ZCode 式)。 - * 面板承载所有标签的通用机制:标签条、˅ 标签列表下拉(逐个关闭、收起面板)、 - * 标签右键菜单(关闭/关闭其他)、空态「打开标签页」宫格、左缘宽度拖拽。 + * 面板承载所有标签的通用机制:平铺标签条(所有打开的 tab 一起展示,点击切换, + * 悬停显示关闭、右键菜单:关闭/关闭其他/收起面板)、+ 新增标签、 + * 空态「打开标签页」宫格、左缘宽度拖拽。 * 具体标签(浏览器等)通过 tabs 传入内容,与面板机制完全解耦; * 新增标签类型只需在宿主的标签注册表里登记,面板零改动。 */ @@ -58,7 +59,6 @@ export function SidePanel({ onOverlayChange }: SidePanelProps): JSX.Element { const dragRef = useRef<{ startX: number; startWidth: number } | null>(null) - const [listOpen, setListOpen] = useState(false) const [plusOpen, setPlusOpen] = useState(false) const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; tabId: string } | null>(null) @@ -86,26 +86,25 @@ export function SidePanel({ // 菜单打开时 Esc 关闭 useEffect(() => { - if (!listOpen && !plusOpen && !ctxMenu) return + if (!plusOpen && !ctxMenu) return const onKey = (e: KeyboardEvent): void => { if (e.key === 'Escape') { - setListOpen(false) setPlusOpen(false) setCtxMenu(null) } } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) - }, [listOpen, plusOpen, ctxMenu]) + }, [plusOpen, ctxMenu]) // 覆盖层开合上报(宿主借此暂停原生视图)。卸载必须归位:面板在菜单开着时 - // 被收起(˅ 列表「收起面板」与 setListOpen 同批提交,归位 effect 不再执行), + // 被收起(与 setCtxMenu/setPlusOpen 同批提交,归位 effect 不再执行), // 宿主的 suspended 会永久卡 true,下次展开面板时原生视图被隐藏成一片空白。 // cleanup 在依赖变化时也会短暂触发 false→当前值,同一提交内批处理合并不闪烁。 useEffect(() => { - onOverlayChange?.(listOpen || plusOpen || ctxMenu !== null) + onOverlayChange?.(plusOpen || ctxMenu !== null) return () => onOverlayChange?.(false) - }, [listOpen, plusOpen, ctxMenu, onOverlayChange]) + }, [plusOpen, ctxMenu, onOverlayChange]) const current = tabs.find((t) => t.id === activeId) ?? tabs[tabs.length - 1] ?? null @@ -125,81 +124,45 @@ export function SidePanel({ {tabs.length > 0 && (
- {/* 面板 tag 本身即下拉按钮(与网页 tab 条的 ˅ 列表按钮形态区分) */} -
- - - {listOpen && ( - <> -
setListOpen(false)} /> -
-
打开的tab
- {tabs.map((tab) => ( -
{ - onSelectTab(tab.id) - setListOpen(false) - }} - className="group flex cursor-default items-center gap-2 px-2 py-1.5 hover:bg-hover" - > - - {tab.title} - -
- ))} -
+ {/* 标签条:所有打开的 tab 平铺展示(浏览器式);超出宽度横向滚动 */} +
+ {tabs.map((tab) => { + const active = tab.id === current?.id + return ( +
onSelectTab(tab.id)} + onContextMenu={(e) => { + e.preventDefault() + setCtxMenu({ + x: Math.min(e.clientX, window.innerWidth - 170), + y: Math.min(e.clientY, window.innerHeight - 140), + tabId: tab.id + }) + }} + className={`group flex h-7 shrink-0 cursor-default select-none items-center gap-1.5 rounded-md px-2 text-[12px] transition-colors ${ + active ? 'bg-inset text-ink' : 'text-ink-3 hover:bg-hover hover:text-ink-2' + }`} + > + + {tab.title}
- - )} + ) + })}
{/* 新增标签:列出注册表中的类型,已打开的置灰 */} @@ -220,7 +183,7 @@ export function SidePanel({ {plusOpen && ( <>
setPlusOpen(false)} /> -
+
{available.map(({ id, title, icon: Icon }) => { const opened = tabs.some((t) => t.id === id) return ( @@ -285,6 +248,18 @@ export function SidePanel({ 关闭其他标签页 +
+
)} @@ -294,7 +269,7 @@ export function SidePanel({ {tabs.length === 0 ? ( ) : ( -
{current?.content ?? null}
+
{current?.content ?? null}
)} )