Files
opsxshell/src/app/AppShell.tsx
T

191 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AppShell — 应用壳
*
* 窗口拖拽约定(Tauri overlay title bar):
* - 仅使用 data-tauri-drag-region 处理拖拽和双击缩放
* - 不要再叠加 startDragging() 或 CSS app-region: drag,两者会抢鼠标导致窗口拖不动
* - 标签栏空白区也由 Tauri 原生拖拽区处理
*
* RDP/VNC 全屏时只隐藏 chrome,保持 TabContent 挂载,避免会话 remount 立刻退出全屏。
*/
import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { ChevronsRight } from 'lucide-react'
import Sidebar from '../components/layout/Sidebar'
import TabBar from '../components/layout/TabBar'
import TabContent from '../components/layout/TabContent'
import StatusBar from '../components/layout/StatusBar'
import type { Session } from '../stores'
import { useDesktopFullscreenStore } from '../stores/desktopFullscreenStore'
import { getDesktopPlatform } from '../platform/runtime'
export interface AppShellProps {
activeTabId: string | null
isMobile: boolean
sidebarCollapsed: boolean
mobileSidebarOpen: boolean
onToggleSidebar: () => void
onToggleMobileSidebar: () => void
onCloseMobileSidebar: () => void
onNewSession: (group?: string) => void
onQuickConnect: () => void
onRdpConnect?: () => void
onVncConnect?: () => void
onImportSessions?: () => void
onOpenLauncher: () => void
onEditSession: (session: Session) => void
children?: ReactNode
}
function TitleBar() {
const platform = getDesktopPlatform()
// Overlay title bar is macOS-only; Win/Linux keep the native chrome.
if (platform !== 'macos') return null
return (
<header
data-tauri-drag-region
className="bg-dark-900/95 relative z-20 flex h-10 flex-shrink-0 select-none items-center border-b border-dark-800"
>
<div data-tauri-drag-region className="h-full w-[78px] flex-shrink-0" aria-hidden />
<div data-tauri-drag-region className="flex h-full min-w-0 flex-1 items-center justify-center px-2" />
<div data-tauri-drag-region className="h-full w-[78px] flex-shrink-0" aria-hidden />
</header>
)
}
function MobileMenuButton({ onClick }: { onClick: () => void }) {
const { t } = useTranslation('common')
return (
<button
type="button"
onClick={onClick}
className="titlebar-no-drag flex h-10 w-10 items-center justify-center text-txt-muted transition-colors hover:bg-dark-800 hover:text-content-primary"
aria-label={t('sidebar.toggle')}
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
)
}
function ExpandSidebarButton({ onClick }: { onClick: () => void }) {
const { t } = useTranslation('common')
return (
<button
type="button"
onClick={onClick}
className="titlebar-no-drag border-dark-700/30 hover:bg-dark-700/50 group flex h-10 w-9 flex-shrink-0 items-center justify-center border-r text-txt-faint transition-colors hover:text-txt-muted"
title={t('sidebar.expand')}
aria-label={t('sidebar.expandAria')}
>
<ChevronsRight className="h-3.5 w-3.5 opacity-70 transition-opacity group-hover:opacity-100" strokeWidth={1.75} />
</button>
)
}
export default function AppShell({
activeTabId,
isMobile,
sidebarCollapsed,
mobileSidebarOpen,
onToggleSidebar,
onToggleMobileSidebar,
onCloseMobileSidebar,
onNewSession,
onQuickConnect,
onRdpConnect,
onVncConnect,
onImportSessions,
onOpenLauncher,
onEditSession,
children,
}: AppShellProps) {
const handleSidebarNewSession = (group?: string) => {
onNewSession(group)
if (isMobile) onCloseMobileSidebar()
}
const handleSidebarQuickConnect = () => {
onQuickConnect()
if (isMobile) onCloseMobileSidebar()
}
const handleSidebarEditSession = (session: Session) => {
onEditSession(session)
if (isMobile) onCloseMobileSidebar()
}
const desktopSidebarHidden = !isMobile && sidebarCollapsed
const desktopFullscreen = useDesktopFullscreenStore((state) => state.active)
return (
<div className={`app-shell text-content-primary ${desktopFullscreen ? 'bg-black' : 'bg-dark-900'}`}>
{!desktopFullscreen && <TitleBar />}
<div className="flex min-h-0 flex-1 overflow-hidden">
{!desktopFullscreen && isMobile && mobileSidebarOpen && (
<div
className="titlebar-no-drag fixed inset-0 z-40 bg-black/50 transition-opacity duration-300"
onClick={onCloseMobileSidebar}
aria-hidden
/>
)}
{!desktopFullscreen && (
<aside
className={`titlebar-no-drag h-full min-h-0 flex-shrink-0 transition-[width,transform] duration-200 ease-in-out ${
isMobile
? `fixed inset-y-0 left-0 top-10 z-50 ${mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`
: desktopSidebarHidden
? 'w-0 overflow-hidden border-0'
: ''
} `}
aria-hidden={desktopSidebarHidden}
>
<Sidebar
onNewSession={handleSidebarNewSession}
onQuickConnect={handleSidebarQuickConnect}
onEditSession={handleSidebarEditSession}
onCollapse={isMobile ? undefined : onToggleSidebar}
/>
</aside>
)}
<main className={`flex min-h-0 min-w-0 flex-1 flex-col ${desktopFullscreen ? 'bg-black' : 'bg-dark-800'}`}>
{!desktopFullscreen && (
<div className="flex items-stretch">
{isMobile && <MobileMenuButton onClick={onToggleMobileSidebar} />}
{desktopSidebarHidden && <ExpandSidebarButton onClick={onToggleSidebar} />}
<div className="min-w-0 flex-1">
<TabBar onOpenLauncher={onOpenLauncher} />
</div>
</div>
)}
<div className="titlebar-no-drag min-h-0 flex-1 overflow-hidden">
<TabContent
activeTabId={activeTabId}
onNewSession={onNewSession}
onQuickConnect={onQuickConnect}
onRdpConnect={onRdpConnect}
onVncConnect={onVncConnect}
onImportSessions={onImportSessions}
/>
</div>
</main>
</div>
{!desktopFullscreen && (
<div className="titlebar-no-drag flex-shrink-0">
<StatusBar />
</div>
)}
<div className="contents">{children}</div>
</div>
)
}