import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; import { usePersistentState } from '../lib/use-persistent-state'; import { Footer } from './Footer'; import { PageActionsSlotContext } from './page-actions'; import { SidebarChromeContext } from './sidebar-chrome'; import { Sidebar } from './Sidebar'; import { clampSidebarWidth, SIDEBAR_DEFAULT_WIDTH_REM, SidebarResizer } from './SidebarResizer'; import { TopBar } from './TopBar'; export function AppLayout(): React.JSX.Element { const { t } = useTranslation(); const [sidebarCollapsed, setSidebarCollapsed] = usePersistentState('ui.sidebar.collapsed', false); const [sidebarWidth, setSidebarWidth] = usePersistentState( 'ui.sidebar.width', SIDEBAR_DEFAULT_WIDTH_REM, ); const [forcedHidden, setForcedHidden] = useState(false); const collapsed = sidebarCollapsed || forcedHidden; // Clamp on read too — the persisted value may predate a bounds change. const widthRem = clampSidebarWidth(sidebarWidth); // The TopBar registers its page-scoped elements here; the active page // portals its icon actions (#101) and presence strip (#102) into them. const [actionsElement, setActionsElement] = useState(null); const [presenceElement, setPresenceElement] = useState(null); const [statusElement, setStatusElement] = useState(null); const actionsSlot = useMemo( () => ({ element: actionsElement, setElement: setActionsElement, presenceElement, setPresenceElement, statusElement, setStatusElement, }), [actionsElement, presenceElement, statusElement], ); // Ctrl/Cmd+\ toggles the sidebar (same shortcut as Notion), regardless of // which element has focus. useEffect(() => { function handleKeyDown(event: KeyboardEvent): void { if ((event.ctrlKey || event.metaKey) && event.key === '\\') { event.preventDefault(); setSidebarCollapsed(!sidebarCollapsed); } } window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [sidebarCollapsed, setSidebarCollapsed]); return (
{/* First tab stop: jump over topbar + sidebar (#166, WCAG 2.4.1). */} {t('layout.skipToContent')} setSidebarCollapsed(!sidebarCollapsed)} />
); }