"use client"; import type { ComponentPropsWithRef, ReactNode } from "react"; import { AnimatedSidebarProvider, type AnimatedSidebarProviderProps, } from "@/components/motion/animated-sidebar"; import { cn } from "@/lib/utils"; export interface WorkspaceShellProps extends AnimatedSidebarProviderProps { sidebar?: ReactNode; tabBar?: ReactNode; mobileHeader?: ReactNode; header?: ReactNode; footer?: ReactNode; /** content: this pane scrolls; none: the editor/chat child owns scrolling. */ scroll?: "content" | "none"; contentProps?: Omit, "children">; } const hasSlot = (node: ReactNode) => node != null && typeof node !== "boolean"; /** The beUI sidebar provider owns navigation state; the host owns tabs and data. * Set a height on the shell. Tabs and content share one alignment line, and the * first active tab removes only the adjoining top-left corner of the surface. * data-workspace-shell identifies this composition without replacing beUI's * own data-slot="sidebar-wrapper" or introducing a second layout wrapper. */ export function WorkspaceShell({ sidebar, tabBar, mobileHeader, header, footer, children, className, style, scroll = "content", contentProps, ...props }: WorkspaceShellProps) { const sidebarInset = hasSlot(sidebar) && "md:ml-[var(--workspace-gap,0px)]"; return ( {sidebar}
{hasSlot(mobileHeader) &&
{mobileHeader}
} {hasSlot(tabBar) &&
{tabBar}
}
{hasSlot(header) &&
{header}
}
{children}
{hasSlot(footer) &&
{footer}
}
); } /** Invoke synchronously from a user action. null means the browser blocked it. * Only the host decides whether to remove a tab and what state may be copied. */ export function openWorkspaceWindow(href: string | URL, prepare?: (popup: Window) => void): Window | null { const url = new URL(href, window.location.href); if (!["http:", "https:"].includes(url.protocol) || url.origin !== window.location.origin || url.username || url.password) { throw new Error("工作区窗口必须使用不含凭据的同源 HTTP(S) 地址。"); } const popup = window.open("about:blank", "_blank", "popup,width=1100,height=760"); if (!popup) return null; try { popup.opener = null; prepare?.(popup); popup.location.replace(url.href); return popup; } catch (error) { popup.close(); throw error; } }