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}
)}
)