import { Fragment, useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactNode } from "react"; import { IconButton, IconClose, IconMenu, IconSidebar, Menu, ThemeSwitch } from "../ui/index.ts"; import "./shell.css"; // Оболочка AURA: закреплённая боковая панель (240 px, свёртка до 64 px) и страница справа; прокрутка — у окна. // Узкий экран (<640 px): панели в потоке нет — закреплённая строка 56 px с кнопкой «Меню», панель выезжает поверх. export type NavItem = { href: string; label: string; icon: ReactNode }; // ссылка по умолчанию — ; с роутером: link={({ href, ...p }) => } export type ShellLink = (p: { href: string; className: string; "aria-current"?: "page"; title?: string; onClick?: () => void; children: ReactNode; }) => ReactNode; const plainLink: ShellLink = ({ href, ...rest }) => ; type Props = { brand: string; // название приложения, Bold 16/24 nav: NavItem[]; current: string | null; // href активного раздела; null — активного нет link?: ShellLink; top?: (collapsed: boolean) => ReactNode; // под названием: выбор проекта/аккаунта и т. п. bottom?: (collapsed: boolean) => ReactNode; // над подвалом: состояние данных, кнопка синхронизации topbarEnd?: ReactNode; // правый край строки на телефоне (компактное состояние) menu?: (close: () => void) => ReactNode; // пункты меню «Ещё» в подвале (MenuItem …) storageKey?: string; // ключ свёртки панели в localStorage wide?: boolean; // страница во всю ширину (доски, графики), иначе до 1440 px children: ReactNode; }; const NARROW = "(max-width: 639px)"; const subscribe = (cb: () => void) => { if (typeof matchMedia !== "function") return () => {}; const m = matchMedia(NARROW); m.addEventListener?.("change", cb); return () => m.removeEventListener?.("change", cb); }; const isNarrow = () => typeof matchMedia === "function" && matchMedia(NARROW).matches; export function AppShell({ wide, children, ...p }: Props) { const narrow = useSyncExternalStore(subscribe, isNarrow); return (