Files
app/src/admin/ui/SidebarNav.tsx
BOHA fd2e613aa5 feat(ui): sidebar icon refresh + unified detail-page headers
Sidebar (user-approved design, docs/superpowers/specs/2026-06-12-sidebar-
icons-design.md): all 31 menu items get unique contextual stroke glyphs in
rounded-square tiles tinted per section (Docházka blue, Kniha jízd teal,
Administrativa amber, Sklad violet, Systém slate; new teal/violet/slate
palette entries with channel tokens in both schemes). Replaces the old set
where 13 items shared a glyph (3x people, 3x sliders, ...) and Faktury was a
dollar sign. Odin stays the only solid + animated tile. Uniqueness pinned by
navdata-icons.test.ts (tsconfig.test.json gains jsx for the .tsx import).

Headers: ProjectDetail and OrderDetail now match the Offer/Invoice shell —
number rendered in secondary color at regular weight next to the bold
entity word, and flexWrap on both left header boxes so the title drops
below Zpět on phones instead of overflowing the viewport (the reported
mobile bug on received orders).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 23:37:44 +02:00

192 lines
6.3 KiB
TypeScript

import { NavLink, useLocation } from "react-router-dom";
import Box from "@mui/material/Box";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Typography from "@mui/material/Typography";
import Avatar from "@mui/material/Avatar";
import Button from "@mui/material/Button";
import { useAuth } from "../context/AuthContext";
import { useTheme as useAppTheme } from "../../context/ThemeContext";
import { menuSections, isItemActive, hasItemPermission } from "./navData";
interface SidebarNavProps {
/** Closes the mobile drawer on item click (optional — desktop omits it). */
onNavigate?: () => void;
/** Logout handler, owned by the parent. */
onLogout: () => void;
}
export default function SidebarNav({ onNavigate, onLogout }: SidebarNavProps) {
const { user, hasPermission } = useAuth();
const { theme } = useAppTheme();
const location = useLocation();
const visibleSections = menuSections
.map((section) => ({
...section,
items: section.items.filter((item) =>
hasItemPermission(item, hasPermission),
),
}))
.filter((section) => section.items.length > 0);
return (
<Box
sx={{ display: "flex", flexDirection: "column", height: "100%", p: 1.5 }}
>
{/* Logo header */}
<Box sx={{ px: 1, py: 1 }}>
<Box
component="img"
src={`/api/admin/company-settings/logo?variant=${theme === "dark" ? "dark" : "light"}`}
alt="Logo"
onError={(e) => {
e.currentTarget.src =
theme === "dark"
? "/images/logo-dark.png"
: "/images/logo-light.png";
}}
sx={{
maxHeight: 40,
maxWidth: "100%",
objectFit: "contain",
display: "block",
}}
/>
</Box>
{/* Nav */}
<Box sx={{ flex: 1, overflowY: "auto", mt: 1 }}>
{visibleSections.map((section) => (
<Box key={section.label}>
<Typography
variant="caption"
sx={{
display: "block",
px: 1.5,
py: 0.5,
textTransform: "uppercase",
letterSpacing: ".06em",
fontWeight: 700,
color: "text.secondary",
fontSize: ".64rem",
}}
>
{section.label}
</Typography>
<List disablePadding>
{section.items.map((item) => (
<ListItemButton
key={item.path}
component={NavLink}
to={item.path}
end={item.end}
onClick={onNavigate}
selected={isItemActive(item, location.pathname)}
sx={{
borderRadius: 2,
mb: 0.25,
py: 0.45,
color: "text.secondary",
"&.Mui-selected, &.Mui-selected:hover": {
bgcolor: "background.paper",
color: "primary.main",
fontWeight: 700,
boxShadow: "0 2px 8px rgba(20,20,40,0.07)",
},
}}
>
<ListItemIcon sx={{ minWidth: 36, color: "inherit" }}>
{item.rawIcon ? (
item.icon
) : (
<Box
sx={(t) => ({
width: 26,
height: 26,
// Echoes OdinMark's rounded-square tile; only Odin
// is solid + animated, these are quiet washes.
borderRadius: "32%",
flexShrink: 0,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
bgcolor: `rgba(${t.vars!.palette[section.hue].mainChannel} / 0.12)`,
color: t.vars!.palette[section.hue].main,
"& svg": { width: 16, height: 16 },
})}
>
{item.icon}
</Box>
)}
</ListItemIcon>
<ListItemText
primary={item.label}
slotProps={{
primary: { fontSize: ".82rem", fontWeight: "inherit" },
}}
/>
</ListItemButton>
))}
</List>
</Box>
))}
</Box>
{/* Footer */}
<Box sx={{ pt: 1, mt: 1, borderTop: 1, borderColor: "divider" }}>
<Box
sx={{ display: "flex", alignItems: "center", gap: 1, px: 0.5, mb: 1 }}
>
<Avatar
sx={{
width: 34,
height: 34,
bgcolor: "primary.main",
fontSize: ".8rem",
fontWeight: 700,
fontFamily: "'Urbanist', sans-serif",
}}
>
{user?.fullName?.charAt(0) || user?.username?.charAt(0) || "U"}
</Avatar>
<Box sx={{ minWidth: 0 }}>
<Typography noWrap sx={{ fontSize: ".82rem", fontWeight: 600 }}>
{user?.fullName || user?.username}
</Typography>
<Typography
noWrap
sx={{ fontSize: ".7rem", color: "text.secondary" }}
>
{user?.roleDisplay}
</Typography>
</Box>
</Box>
<Button
fullWidth
color="inherit"
onClick={onLogout}
startIcon={
<svg
viewBox="0 0 24 24"
width="18"
height="18"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
}
>
Odhlásit se
</Button>
</Box>
</Box>
);
}