109 lines
5.3 KiB
TypeScript
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")} />
|
|
</>
|
|
);
|
|
}
|