Files
ttstd-infinite-canvas/web/src/components/layout/app-top-nav.tsx

109 lines
5.3 KiB
TypeScript

import { lazy, Suspense, useEffect, useState, type ReactNode } from "react";
import { useLocation, useNavigate } from "react-router";
import { ModelSetupGuide } from "@/components/layout/model-setup-guide";
import { WorkspaceSidebarNav } from "@/components/layout/workspace-sidebar-nav";
import { readWorkspaceSidebarCollapsed, writeWorkspaceSidebarCollapsed } from "@/components/layout/workspace-sidebar-state";
import { WorkspaceTopBar } from "@/components/layout/workspace-top-bar";
import { WorkspaceTopBarExtensionProvider } from "@/components/layout/workspace-top-bar-extension";
import { cn } from "@/lib/utils";
import { isSpatialWorkbenchPath } from "@/lib/workspace-routes";
const WorkspaceCommandPalette = lazy(() => import("@/components/layout/workspace-command-palette").then((module) => ({ default: module.WorkspaceCommandPalette })));
export function AppWorkspaceShell({ children }: { children: ReactNode }) {
const { pathname } = useLocation();
const navigate = useNavigate();
const [mobileSidebarExpanded, setMobileSidebarExpanded] = useState(false);
const [desktopSidebarCollapsed, setDesktopSidebarCollapsed] = useState(readWorkspaceSidebarCollapsed);
const [paletteOpen, setPaletteOpen] = useState(false);
const hideChrome = pathname.startsWith("/admin") || /^\/canvas\/[^/]+/.test(pathname);
const showGlobalTopBar = !hideChrome;
const spatialWorkbench = isSpatialWorkbenchPath(pathname);
const creationWorkspace = pathname === "/";
const isMobileViewport = () => window.innerWidth < 1024;
const toggleSidebar = () => {
if (isMobileViewport()) {
setMobileSidebarExpanded((current) => !current);
return;
}
setDesktopSidebarCollapsed((current) => {
const next = !current;
writeWorkspaceSidebarCollapsed(next);
return next;
});
};
const expandDesktopSidebar = () => {
setDesktopSidebarCollapsed(false);
writeWorkspaceSidebarCollapsed(false);
};
const handleNavClick = () => {
if (isMobileViewport()) setMobileSidebarExpanded(false);
};
// ⌘K / Ctrl+K 全局呼出搜索面板。
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
event.preventDefault();
setPaletteOpen((open) => !open);
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);
useEffect(() => {
const handleWorkspaceNavigation = (rawEvent: Event) => {
const event = rawEvent as CustomEvent<{ to?: string }>;
if (!event.detail?.to) return;
event.preventDefault();
navigate(event.detail.to);
};
window.addEventListener("workspace:navigate", handleWorkspaceNavigation);
return () => window.removeEventListener("workspace:navigate", handleWorkspaceNavigation);
}, [navigate]);
return (
<>
<WorkspaceTopBarExtensionProvider>
<div className={cn("app-workspace-shell flex h-dvh min-h-0 w-full flex-col overflow-hidden", spatialWorkbench && "is-spatial", creationWorkspace && "is-creation-workspace")}>
{!hideChrome && mobileSidebarExpanded ? <button type="button" className="app-workspace-sidebar-scrim lg:hidden" aria-label="收起侧栏" onClick={() => setMobileSidebarExpanded(false)} /> : null}
<div className="app-workspace-main-row flex min-h-0 min-w-0 flex-1 overflow-hidden">
{!hideChrome ? (
<aside
className={cn(
"app-workspace-sidebar flex h-full shrink-0 flex-col overflow-hidden",
mobileSidebarExpanded && "is-mobile-expanded",
desktopSidebarCollapsed && "is-collapsed",
)}
>
<WorkspaceSidebarNav
collapsed={desktopSidebarCollapsed}
onNavigate={handleNavClick}
onOpenSearch={() => setPaletteOpen(true)}
onExpand={expandDesktopSidebar}
/>
</aside>
) : null}
<div className="app-workspace-stage relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{showGlobalTopBar ? <WorkspaceTopBar sidebarOpen={isMobileViewport() ? mobileSidebarExpanded : !desktopSidebarCollapsed} onToggleSidebar={toggleSidebar} /> : null}
<div className="relative min-h-0 min-w-0 flex-1 overflow-hidden">{children}</div>
</div>
</div>
{paletteOpen ? <Suspense fallback={null}><WorkspaceCommandPalette open onClose={() => setPaletteOpen(false)} /></Suspense> : null}
</div>
</WorkspaceTopBarExtensionProvider>
<ModelSetupGuide hidden={pathname === "/login" || pathname === "/register" || pathname.startsWith("/admin")} />
</>
);
}