/**
* Main application layout.
*
* Provides the persistent sidebar navigation and the main content area
* for all authenticated pages. The sidebar collapses from 240 px to
* icon-only (48 px) on small screens.
*/
import { useCallback, useEffect, useState } from "react";
import {
Badge,
Button,
makeStyles,
mergeClasses,
MessageBar,
MessageBarBody,
MessageBarTitle,
Text,
tokens,
Tooltip,
} from "@fluentui/react-components";
import {
GridRegular,
MapRegular,
ShieldRegular,
SettingsRegular,
HistoryRegular,
ListRegular,
SignOutRegular,
NavigationRegular,
} from "@fluentui/react-icons";
import { NavLink, Outlet, useNavigate } from "react-router-dom";
import { useAuth } from "../providers/AuthProvider";
import { useServerStatus } from "../hooks/useServerStatus";
import { useBlocklistStatus } from "../hooks/useBlocklist";
// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------
const SIDEBAR_FULL = "240px";
const SIDEBAR_COLLAPSED = "48px";
const useStyles = makeStyles({
root: {
display: "flex",
height: "100vh",
overflow: "hidden",
backgroundColor: tokens.colorNeutralBackground3,
},
// Sidebar
sidebar: {
display: "flex",
flexDirection: "column",
width: SIDEBAR_FULL,
minWidth: SIDEBAR_COLLAPSED,
backgroundColor: tokens.colorNeutralBackground1,
borderRightWidth: "1px",
borderRightStyle: "solid",
borderRightColor: tokens.colorNeutralStroke2,
transition: "width 200ms ease",
overflow: "hidden",
flexShrink: 0,
// Honour the OS/browser reduced-motion preference.
"@media (prefers-reduced-motion: reduce)": {
transition: "none",
},
},
sidebarCollapsed: {
width: SIDEBAR_COLLAPSED,
},
sidebarHeader: {
display: "flex",
alignItems: "center",
height: "52px",
paddingLeft: tokens.spacingHorizontalM,
paddingRight: tokens.spacingHorizontalS,
gap: tokens.spacingHorizontalS,
borderBottomWidth: "1px",
borderBottomStyle: "solid",
borderBottomColor: tokens.colorNeutralStroke2,
flexShrink: 0,
},
logo: {
fontWeight: 600,
fontSize: "16px",
whiteSpace: "nowrap",
overflow: "hidden",
color: tokens.colorBrandForeground1,
flexGrow: 1,
},
// Nav items list
navList: {
display: "flex",
flexDirection: "column",
gap: "2px",
padding: tokens.spacingVerticalS,
overflowY: "auto",
flexGrow: 1,
},
navLinkContent: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS,
flexGrow: 1,
},
navLink: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS,
padding: `${tokens.spacingVerticalSNudge} ${tokens.spacingHorizontalS}`,
borderRadius: tokens.borderRadiusMedium,
textDecoration: "none",
color: tokens.colorNeutralForeground2,
whiteSpace: "nowrap",
overflow: "hidden",
":hover": {
backgroundColor: tokens.colorNeutralBackground1Hover,
color: tokens.colorNeutralForeground1,
},
},
navLinkActive: {
backgroundColor: tokens.colorNeutralBackground1Selected,
color: tokens.colorBrandForeground1,
":hover": {
backgroundColor: tokens.colorNeutralBackground1Selected,
},
},
navLabel: {
fontSize: "14px",
lineHeight: "20px",
overflow: "hidden",
textOverflow: "ellipsis",
},
// Sidebar footer (logout)
sidebarFooter: {
borderTopWidth: "1px",
borderTopStyle: "solid",
borderTopColor: tokens.colorNeutralStroke2,
padding: tokens.spacingVerticalS,
flexShrink: 0,
},
versionText: {
display: "block",
color: tokens.colorNeutralForeground4,
fontSize: "11px",
paddingLeft: tokens.spacingHorizontalS,
paddingRight: tokens.spacingHorizontalS,
paddingBottom: tokens.spacingVerticalXS,
whiteSpace: "nowrap",
overflow: "hidden",
},
// Main content
main: {
display: "flex",
flexDirection: "column",
flexGrow: 1,
overflow: "auto",
},
warningBar: {
flexShrink: 0,
paddingLeft: tokens.spacingHorizontalM,
paddingRight: tokens.spacingHorizontalM,
paddingTop: tokens.spacingVerticalXS,
paddingBottom: tokens.spacingVerticalXS,
},
content: {
flexGrow: 1,
maxWidth: "1440px",
width: "100%",
margin: "0 auto",
padding: tokens.spacingVerticalL,
},
});
// ---------------------------------------------------------------------------
// Nav item data
// ---------------------------------------------------------------------------
interface NavItem {
label: string;
to: string;
icon: React.ReactElement;
end?: boolean;
}
const NAV_ITEMS: NavItem[] = [
{ label: "Dashboard", to: "/", icon: