import { useEffect, useRef, useState, type ReactNode } from 'react' import { ChevronDown, Menu, PanelLeftClose, PanelLeftOpen, X } from 'lucide-react' import { useT } from '@/shared/i18n' /** Shared, responsive workspace frame. Content panes own their scroll position. */ export interface AppShellProps { rail?: ReactNode sideHeader?: ReactNode /** Pinned rows above the "工作区" caption (e.g. the personal inbox entry). */ navPinned?: ReactNode nav?: ReactNode sideFooter?: ReactNode /** Directory-style column (explore): the rail stays, the space sidebar goes. */ railOnly?: boolean children: ReactNode } export function AppShell({ rail, sideHeader, navPinned, nav, sideFooter, railOnly = false, children }: AppShellProps) { const { t } = useT() const [navigationOpen, setNavigationOpen] = useState(false) const [collapsed, setCollapsed] = useState(false) const navigation = useRef(null) const menu = useRef(null) useEffect(() => { if (!navigationOpen) return const panel = navigation.current const previous = document.activeElement as HTMLElement | null panel?.querySelector('button, a[href]')?.focus() function onKey(event: KeyboardEvent) { if (event.key === 'Escape') setNavigationOpen(false) if (event.key !== 'Tab' || !panel) return const items = [...panel.querySelectorAll('a[href], button:not([disabled]), input:not([disabled])')].filter(item => item.getClientRects().length) const first = items[0] const last = items[items.length - 1] if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus() } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus() } } window.addEventListener('keydown', onKey) return () => { window.removeEventListener('keydown', onKey) if (previous?.isConnected) previous.focus() } }, [navigationOpen]) useEffect(() => { const viewport = window.matchMedia('(min-width: 761px)') const closeDrawer = () => { if (viewport.matches) setNavigationOpen(false) } viewport.addEventListener('change', closeDrawer) return () => viewport.removeEventListener('change', closeDrawer) }, []) return (
GitData{t('ui.workspaceLabel')}
{navigationOpen ?
{navPinned ? (
{navPinned}
) : null}
{t('ui.workspace')}
{sideFooter ?
{sideFooter}
: null}
{/* Always mounted: `visibility` hides it when the sidebar is open, which keeps it out of the tab order while still letting it fade in and out. */} {children}
) } export interface ShellTopBarProps { icon?: ReactNode title: ReactNode /** Replaces the plain title heading with an interactive node (e.g. the asset search menu). */ titleMenu?: ReactNode topic?: ReactNode /** Page-level view switch, rendered beside the title. */ tabs?: ReactNode actions?: ReactNode } export function ShellTopBar({ icon, title, titleMenu, topic, tabs, actions }: ShellTopBarProps) { return (
{icon ? {icon} : null}
{titleMenu ??

{title}

} {tabs ?
{tabs}
: null}
{topic ?

{topic}

: null}
{actions ?
{actions}
: null}
) }