import { useT } from '@app/i18n' import { Menu, PanelLeftClose, PanelLeftOpen, X } from 'lucide-react' import { type ReactNode, useEffect, useRef, useState } from 'react' /** Shared, responsive workspace frame. Content panes own their scroll position. */ export interface AppShellProps { sideHeader?: ReactNode /** Pinned rows above the "工作区" caption (e.g. the personal inbox entry). */ navPinned?: ReactNode nav?: ReactNode sideFooter?: ReactNode /** Directory-style page (explore): the whole navigation column is dropped. */ noSidebar?: boolean children: ReactNode } export function AppShell({ sideHeader, navPinned, nav, sideFooter, noSidebar = 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}
) }