mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
ui design for schedule
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
/* ============================================================
|
||||
EDR Freight — Header styles
|
||||
============================================================ */
|
||||
|
||||
.fdh-root {
|
||||
display: flex;
|
||||
height: 80px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 0 22px;
|
||||
}
|
||||
|
||||
/* eyebrow above the page title */
|
||||
.fdh-eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.6px;
|
||||
text-transform: uppercase;
|
||||
color: #15803d;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.fdh-eyebrow-dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.16);
|
||||
}
|
||||
|
||||
/* action icon buttons */
|
||||
.fdh-icon-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: #f7f9fb;
|
||||
border: 1px solid #eef1f4;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
transition: all 160ms ease;
|
||||
}
|
||||
.fdh-icon-btn:hover {
|
||||
background: #ffffff;
|
||||
border-color: rgba(21, 128, 61, 0.28);
|
||||
color: #15803d;
|
||||
box-shadow: 0 4px 12px -4px rgba(21, 128, 61, 0.28);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.fdh-icon-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* notification badge */
|
||||
.fdh-badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
min-width: 17px;
|
||||
height: 17px;
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9px;
|
||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
border: 2px solid #ffffff;
|
||||
box-shadow: 0 2px 6px -1px rgba(239, 68, 68, 0.45);
|
||||
}
|
||||
|
||||
.fdh-divider {
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
background: #e9eef3;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
/* user button */
|
||||
.fdh-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 5px 12px 5px 5px;
|
||||
border-radius: 13px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all 160ms ease;
|
||||
}
|
||||
.fdh-user:hover {
|
||||
background: #f7f9fb;
|
||||
border-color: #eef1f4;
|
||||
}
|
||||
|
||||
.fdh-avatar-ring {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
padding: 2px;
|
||||
background: linear-gradient(135deg, #22c55e 0%, #15803d 100%);
|
||||
box-shadow: 0 4px 10px -3px rgba(21, 128, 61, 0.4);
|
||||
}
|
||||
.fdh-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #16a34a 0%, #166534 100%);
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
border: 2px solid #ffffff;
|
||||
}
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
Sun,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { Group, Stack, Text, Avatar, Menu, ActionIcon, Badge, Box } from "@mantine/core";
|
||||
import { Group, Stack, Text, Menu, Tooltip, Box } from "@mantine/core";
|
||||
|
||||
import type { PageMeta } from "./types";
|
||||
import { freightBrand } from "@/theme/freight-brand";
|
||||
import "./FreightDashboardHeader.css";
|
||||
|
||||
export interface FreightDashboardHeaderProps {
|
||||
pageMeta: PageMeta;
|
||||
@@ -39,12 +39,13 @@ const FreightDashboardHeader = ({
|
||||
}: FreightDashboardHeaderProps) => {
|
||||
const initials =
|
||||
userInitials ??
|
||||
userName
|
||||
(userName
|
||||
.split(" ")
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((n) => n[0].toUpperCase())
|
||||
.join("");
|
||||
.join("") ||
|
||||
"U");
|
||||
|
||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
||||
const userMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -74,133 +75,137 @@ const FreightDashboardHeader = ({
|
||||
}, [isUserMenuOpen]);
|
||||
|
||||
return (
|
||||
<header
|
||||
style={{
|
||||
display: "flex",
|
||||
height: "80px",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "16px",
|
||||
padding: "0 24px",
|
||||
// borderBottom: `3px solid ${freightBrand.primary}`,
|
||||
}}
|
||||
>
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="lg" fw={700} truncate style={{ color: freightBrand.primaryDark }}>
|
||||
<header className="fdh-root">
|
||||
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
|
||||
<span className="fdh-eyebrow">
|
||||
<span className="fdh-eyebrow-dot" />
|
||||
Freight Backoffice
|
||||
</span>
|
||||
<Text
|
||||
fw={700}
|
||||
truncate
|
||||
style={{ fontSize: "20px", lineHeight: 1.2, color: "#0f172a", letterSpacing: "-0.4px" }}
|
||||
>
|
||||
{pageMeta.title}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" truncate>
|
||||
<Text size="sm" truncate style={{ color: "#94a3b8", lineHeight: 1.35 }}>
|
||||
{pageMeta.subtitle}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Group gap={10} wrap="nowrap">
|
||||
{enableThemeToggle && (
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size={40}
|
||||
radius="lg"
|
||||
onClick={onToggleTheme}
|
||||
style={{
|
||||
background: "var(--mantine-color-gray-1)",
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
color: "var(--mantine-color-gray-7)",
|
||||
}}
|
||||
<Tooltip
|
||||
label={theme === "dark" ? "Light mode" : "Dark mode"}
|
||||
withArrow
|
||||
openDelay={300}
|
||||
>
|
||||
{theme === "dark" ? <Sun size={18} /> : <Moon size={18} />}
|
||||
</ActionIcon>
|
||||
<button
|
||||
type="button"
|
||||
className="fdh-icon-btn"
|
||||
onClick={onToggleTheme}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === "dark" ? <Sun size={18} /> : <Moon size={18} />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size={40}
|
||||
radius="lg"
|
||||
style={{
|
||||
background: "var(--mantine-color-gray-1)",
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
color: "var(--mantine-color-gray-7)",
|
||||
}}
|
||||
>
|
||||
<Languages size={18} />
|
||||
</ActionIcon>
|
||||
<Tooltip label="Language" withArrow openDelay={300}>
|
||||
<button type="button" className="fdh-icon-btn" aria-label="Language">
|
||||
<Languages size={18} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size={40}
|
||||
radius="lg"
|
||||
style={{
|
||||
background: "var(--mantine-color-gray-1)",
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
color: "var(--mantine-color-gray-7)",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<MessageSquare size={18} />
|
||||
<Badge
|
||||
size="xs"
|
||||
color="red"
|
||||
circle
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-3px",
|
||||
right: "-3px",
|
||||
}}
|
||||
/>
|
||||
</ActionIcon>
|
||||
<Tooltip label="Messages" withArrow openDelay={300}>
|
||||
<button type="button" className="fdh-icon-btn" aria-label="Messages">
|
||||
<MessageSquare size={18} />
|
||||
<span className="fdh-badge">3</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size={40}
|
||||
radius="lg"
|
||||
style={{
|
||||
background: "var(--mantine-color-gray-1)",
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
color: "var(--mantine-color-gray-7)",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Bell size={18} />
|
||||
<Badge
|
||||
size="xs"
|
||||
color="red"
|
||||
circle
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-3px",
|
||||
right: "-3px",
|
||||
}}
|
||||
/>
|
||||
</ActionIcon>
|
||||
<Tooltip label="Notifications" withArrow openDelay={300}>
|
||||
<button
|
||||
type="button"
|
||||
className="fdh-icon-btn"
|
||||
aria-label="Notifications"
|
||||
>
|
||||
<Bell size={18} />
|
||||
<span className="fdh-badge">5</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Menu position="bottom-end" shadow="md" opened={isUserMenuOpen} onOpen={() => setIsUserMenuOpen(true)} onClose={() => setIsUserMenuOpen(false)}>
|
||||
<div className="fdh-divider" />
|
||||
|
||||
<Menu
|
||||
position="bottom-end"
|
||||
shadow="lg"
|
||||
radius="md"
|
||||
width={240}
|
||||
opened={isUserMenuOpen}
|
||||
onOpen={() => setIsUserMenuOpen(true)}
|
||||
onClose={() => setIsUserMenuOpen(false)}
|
||||
>
|
||||
<Menu.Target>
|
||||
<Group gap="sm" p="xs" style={{ cursor: "pointer", borderRadius: "12px" }}>
|
||||
<Avatar name={initials} color="green" size="md" styles={{ root: { background: freightBrand.primary } }} />
|
||||
<ChevronDown size={16} style={{ transition: "transform 0.2s", transform: isUserMenuOpen ? "rotate(180deg)" : "rotate(0deg)" }} />
|
||||
</Group>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item disabled>
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" fw={600}>
|
||||
<div className="fdh-user">
|
||||
<div className="fdh-avatar-ring">
|
||||
<div className="fdh-avatar">{initials}</div>
|
||||
</div>
|
||||
<Stack gap={0} style={{ minWidth: 0 }} visibleFrom="sm">
|
||||
<Text
|
||||
size="sm"
|
||||
fw={600}
|
||||
truncate
|
||||
style={{ color: "#0f172a", lineHeight: 1.25, maxWidth: 140 }}
|
||||
>
|
||||
{userName}
|
||||
</Text>
|
||||
{userEmail && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{userEmail}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
size="xs"
|
||||
truncate
|
||||
style={{ color: "#94a3b8", lineHeight: 1.25, maxWidth: 140 }}
|
||||
>
|
||||
{userEmail ?? "Administrator"}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Menu.Item>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
style={{
|
||||
color: "#94a3b8",
|
||||
flexShrink: 0,
|
||||
transition: "transform 0.2s",
|
||||
transform: isUserMenuOpen ? "rotate(180deg)" : "rotate(0deg)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Box px="sm" py="xs">
|
||||
<Group gap={10} wrap="nowrap">
|
||||
<div className="fdh-avatar-ring">
|
||||
<div className="fdh-avatar">{initials}</div>
|
||||
</div>
|
||||
<Stack gap={0} style={{ minWidth: 0 }}>
|
||||
<Text size="sm" fw={600} truncate style={{ color: "#0f172a" }}>
|
||||
{userName}
|
||||
</Text>
|
||||
{userEmail && (
|
||||
<Text size="xs" truncate style={{ color: "#94a3b8" }}>
|
||||
{userEmail}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
</Box>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
leftSection={<User size={14} />}
|
||||
leftSection={<User size={15} />}
|
||||
onClick={() => setIsUserMenuOpen(false)}
|
||||
>
|
||||
Profile
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<LogOut size={14} />}
|
||||
leftSection={<LogOut size={15} />}
|
||||
color="red"
|
||||
onClick={() => {
|
||||
setIsUserMenuOpen(false);
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
/* ============================================================
|
||||
EDR Freight — Sidebar styles
|
||||
Polished, professional navigation surface.
|
||||
============================================================ */
|
||||
|
||||
.fsb-aside {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #eef1f4;
|
||||
background: #ffffff;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(15, 23, 42, 0.04),
|
||||
0 8px 24px -16px rgba(15, 23, 42, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ---- Brand header ---- */
|
||||
.fsb-brand {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
height: 80px;
|
||||
padding: 0 20px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fsb-brand::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(120px 80px at 24px 18px, rgba(34, 197, 94, 0.08), transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fsb-logo {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 13px;
|
||||
background: linear-gradient(135deg, #22c55e 0%, #15803d 60%, #166534 100%);
|
||||
box-shadow:
|
||||
0 6px 16px -4px rgba(21, 128, 61, 0.45),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ---- Nav scroll region ---- */
|
||||
.fsb-nav {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 14px 12px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.fsb-nav::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.fsb-nav::-webkit-scrollbar-thumb {
|
||||
background: #e2e8f0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.fsb-nav::-webkit-scrollbar-thumb:hover {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
.fsb-nav::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.fsb-section-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.7px;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
padding: 0 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* ---- Top-level item ---- */
|
||||
.fsb-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
width: 100%;
|
||||
padding: 9px 12px;
|
||||
border-radius: 11px;
|
||||
cursor: pointer;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background-color 160ms ease,
|
||||
color 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.fsb-item:hover {
|
||||
background-color: #f5f7fa;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.fsb-item[data-active="true"] {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(34, 197, 94, 0.12) 0%,
|
||||
rgba(21, 128, 61, 0.06) 100%
|
||||
);
|
||||
color: #15803d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fsb-item[data-active="true"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 22px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
background: linear-gradient(180deg, #22c55e 0%, #15803d 100%);
|
||||
}
|
||||
|
||||
.fsb-item-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ---- Icon well ---- */
|
||||
.fsb-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
border-radius: 9px;
|
||||
flex-shrink: 0;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
transition: all 160ms ease;
|
||||
}
|
||||
|
||||
.fsb-item:hover .fsb-icon {
|
||||
background: #e6ebf1;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.fsb-item[data-active="true"] .fsb-icon {
|
||||
background: linear-gradient(135deg, #22c55e 0%, #15803d 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 5px 12px -2px rgba(21, 128, 61, 0.45);
|
||||
}
|
||||
|
||||
.fsb-chevron {
|
||||
flex-shrink: 0;
|
||||
color: #94a3b8;
|
||||
transition: transform 220ms ease;
|
||||
}
|
||||
|
||||
/* ---- Nested branch ---- */
|
||||
.fsb-branch {
|
||||
margin: 2px 0 2px 22px;
|
||||
padding-left: 12px;
|
||||
border-left: 1.5px solid #eef2f6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* group header (non-navigable) */
|
||||
.fsb-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
.fsb-group:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
.fsb-group-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4px;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.fsb-group[data-active="true"] .fsb-group-label {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
/* child leaf */
|
||||
.fsb-child {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background-color 150ms ease,
|
||||
color 150ms ease;
|
||||
}
|
||||
.fsb-child:hover {
|
||||
background-color: #f5f7fa;
|
||||
color: #0f172a;
|
||||
}
|
||||
.fsb-child[data-active="true"] {
|
||||
color: #15803d;
|
||||
font-weight: 600;
|
||||
background-color: rgba(21, 128, 61, 0.08);
|
||||
}
|
||||
|
||||
.fsb-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
background: #cbd5e1;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
.fsb-child:hover .fsb-dot {
|
||||
background: #94a3b8;
|
||||
}
|
||||
.fsb-child[data-active="true"] .fsb-dot {
|
||||
background: #15803d;
|
||||
box-shadow: 0 0 0 3px rgba(21, 128, 61, 0.16);
|
||||
}
|
||||
|
||||
/* ---- Footer status card ---- */
|
||||
.fsb-footer {
|
||||
flex-shrink: 0;
|
||||
padding: 12px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
.fsb-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 11px;
|
||||
background: linear-gradient(135deg, #f0fdf4 0%, #f8fafc 100%);
|
||||
border: 1px solid #e7f3ec;
|
||||
}
|
||||
.fsb-pulse {
|
||||
position: relative;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.fsb-pulse::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
animation: fsb-pulse 2s ease-out infinite;
|
||||
}
|
||||
@keyframes fsb-pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2.6);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import {
|
||||
type MouseEvent,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { ChevronDown, ChevronRight, Train } from "lucide-react";
|
||||
import { Stack, Group, Text, Box, UnstyledButton, NavLink } from "@mantine/core";
|
||||
import { ChevronDown, Train } from "lucide-react";
|
||||
import { Box, Stack, Text } from "@mantine/core";
|
||||
|
||||
import type { SidebarItem, SidebarSection } from "./types";
|
||||
import { freightBrand } from "@/theme/freight-brand";
|
||||
import "./FreightSidebar.css";
|
||||
|
||||
export interface FreightSidebarProps {
|
||||
sections: SidebarSection[];
|
||||
@@ -105,7 +106,7 @@ const FreightSidebar = ({
|
||||
children: SidebarItem[],
|
||||
depth: number,
|
||||
parentKey: string,
|
||||
) =>
|
||||
): ReactNode =>
|
||||
children.map((child) => {
|
||||
const key = sidebarItemKey(child, parentKey);
|
||||
const isGroup = Boolean(child.children?.length) && !child.href;
|
||||
@@ -115,87 +116,50 @@ const FreightSidebar = ({
|
||||
const groupActive = branchContainsActive(child.children!);
|
||||
|
||||
return (
|
||||
<Stack key={key} gap={4}>
|
||||
<UnstyledButton
|
||||
<div key={key}>
|
||||
<button
|
||||
type="button"
|
||||
className="fsb-group"
|
||||
data-active={groupActive}
|
||||
onClick={() => toggleExpanded(key)}
|
||||
style={{
|
||||
background: groupActive ? freightBrand.mutedBg : "transparent",
|
||||
padding: "8px 12px",
|
||||
borderRadius: "8px",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Text size="xs" fw={600} style={{ color: groupActive ? freightBrand.primary : undefined }} c={groupActive ? undefined : "dimmed"} tt="uppercase">
|
||||
{child.label}
|
||||
</Text>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
style={{
|
||||
transform: isOpen ? "rotate(0deg)" : "rotate(-90deg)",
|
||||
transition: "transform 0.2s",
|
||||
color: groupActive ? freightBrand.primary : "var(--mantine-color-gray-5)",
|
||||
}}
|
||||
/>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
<span className="fsb-group-label">{child.label}</span>
|
||||
<ChevronDown
|
||||
size={13}
|
||||
className="fsb-chevron"
|
||||
style={{
|
||||
transform: isOpen ? "rotate(0deg)" : "rotate(-90deg)",
|
||||
color: groupActive ? "#15803d" : undefined,
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<Stack gap={2} style={{ paddingLeft: "12px", borderLeft: `2px solid ${freightBrand.mutedBorder}` }}>
|
||||
<div className="fsb-branch">
|
||||
{renderNavBranch(child.children!, depth + 1, key)}
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!child.href) return null;
|
||||
|
||||
const childHref = child.href.toLowerCase();
|
||||
const childActiveHref = isHrefActive(childHref);
|
||||
const childActive = isHrefActive(child.href);
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
<a
|
||||
key={key}
|
||||
component="a"
|
||||
href={child.href}
|
||||
onClick={(e) => navigateTo(e as any, child.href!)}
|
||||
label={child.label}
|
||||
active={childActiveHref}
|
||||
color="green"
|
||||
style={{
|
||||
borderRadius: "8px",
|
||||
cursor: "pointer",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
rightSection={<ChevronRight size={16} />}
|
||||
/>
|
||||
className="fsb-child"
|
||||
data-active={childActive}
|
||||
onClick={(e) => navigateTo(e, child.href!)}
|
||||
>
|
||||
<span className="fsb-dot" />
|
||||
<span className="fsb-item-label">{child.label}</span>
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
const renderIconWell = (icon: React.ReactNode, active: boolean) => {
|
||||
if (!icon) return null;
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
borderRadius: "8px",
|
||||
flexShrink: 0,
|
||||
background: active ? freightBrand.gradient : "var(--mantine-color-gray-1)",
|
||||
color: active ? "white" : "var(--mantine-color-gray-6)",
|
||||
boxShadow: active ? freightBrand.shadowSm : "none",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTopLevelItem = (item: SidebarItem) => {
|
||||
if (!item.href) return null;
|
||||
|
||||
@@ -207,132 +171,92 @@ const FreightSidebar = ({
|
||||
const isCurrentItem = hasChildren
|
||||
? activePath === itemHref
|
||||
: isHrefActive(itemHref);
|
||||
const isSectionActive = childActive && !isCurrentItem;
|
||||
const isActive = isCurrentItem || isSectionActive;
|
||||
const isActive = isCurrentItem || childActive;
|
||||
const isOpen = expanded[item.href] ?? false;
|
||||
const leafActive = isCurrentItem && !hasChildren;
|
||||
|
||||
return (
|
||||
<Stack key={item.href} gap={0}>
|
||||
<NavLink
|
||||
component="a"
|
||||
<Box key={item.href}>
|
||||
<a
|
||||
href={item.href}
|
||||
onClick={(e) => navigateTo(e as any, item.href!)}
|
||||
label={item.label}
|
||||
leftSection={renderIconWell(item.icon, isActive)}
|
||||
active={leafActive}
|
||||
color="green"
|
||||
variant="light"
|
||||
style={{
|
||||
borderRadius: "10px",
|
||||
cursor: "pointer",
|
||||
fontSize: "14px",
|
||||
fontWeight: 500,
|
||||
padding: "8px 10px",
|
||||
}}
|
||||
rightSection={
|
||||
hasChildren ? (
|
||||
<ChevronDown
|
||||
size={16}
|
||||
style={{
|
||||
transform: isOpen ? "rotate(0deg)" : "rotate(-90deg)",
|
||||
transition: "transform 0.2s",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toggleExpanded(item.href!);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ChevronRight size={16} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
className="fsb-item"
|
||||
data-active={isActive}
|
||||
onClick={(e) => navigateTo(e, item.href!)}
|
||||
>
|
||||
{item.icon && <span className="fsb-icon">{item.icon}</span>}
|
||||
<span className="fsb-item-label">{item.label}</span>
|
||||
{hasChildren && (
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className="fsb-chevron"
|
||||
style={{
|
||||
transform: isOpen ? "rotate(0deg)" : "rotate(-90deg)",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleExpanded(item.href!);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
|
||||
{hasChildren && isOpen && (
|
||||
<Stack gap={4} style={{ paddingLeft: "16px", borderLeft: `2px solid ${freightBrand.mutedBorder}` }}>
|
||||
<div className="fsb-branch">
|
||||
{renderNavBranch(item.children!, 0, item.href)}
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="aside"
|
||||
style={{
|
||||
height: "100%",
|
||||
maxHeight: "100%",
|
||||
width: "280px",
|
||||
flexShrink: 0,
|
||||
borderRadius: "12px",
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
background: "white",
|
||||
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.05)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Group
|
||||
gap={12}
|
||||
px="lg"
|
||||
py="md"
|
||||
style={{
|
||||
borderBottom: "1px solid var(--mantine-color-gray-2)",
|
||||
flexShrink: 0,
|
||||
height: "80px",
|
||||
}}
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "44px",
|
||||
height: "44px",
|
||||
borderRadius: "12px",
|
||||
background: freightBrand.gradient,
|
||||
boxShadow: freightBrand.shadow,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Train size={24} color="white" strokeWidth={2} />
|
||||
</Box>
|
||||
<Stack gap={0} style={{ minWidth: 0 }}>
|
||||
<Text size="md" fw={700} style={{ letterSpacing: "-0.3px", lineHeight: 1.2 }}>
|
||||
<Box component="aside" className="fsb-aside">
|
||||
<div className="fsb-brand">
|
||||
<div className="fsb-logo">
|
||||
<Train size={23} color="white" strokeWidth={2.1} />
|
||||
</div>
|
||||
<Stack gap={1} style={{ minWidth: 0, position: "relative", zIndex: 1 }}>
|
||||
<Text
|
||||
size="md"
|
||||
fw={700}
|
||||
style={{ letterSpacing: "-0.3px", lineHeight: 1.2, color: "#0f172a" }}
|
||||
>
|
||||
EDR Freight
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" fw={500} style={{ letterSpacing: "0.3px" }}>
|
||||
Backoffice
|
||||
<Text
|
||||
size="xs"
|
||||
fw={600}
|
||||
style={{ letterSpacing: "0.4px", color: "#94a3b8" }}
|
||||
>
|
||||
Backoffice Console
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<Stack
|
||||
component="nav"
|
||||
gap="lg"
|
||||
p="md"
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowY: "auto",
|
||||
overscrollBehavior: "contain",
|
||||
}}
|
||||
>
|
||||
<nav className="fsb-nav">
|
||||
{sections.map((section) => (
|
||||
<Stack key={section.title} gap={8}>
|
||||
<Text size="xs" fw={600} c="dimmed" tt="uppercase" style={{ letterSpacing: "0.5px", paddingLeft: "8px" }}>
|
||||
{section.title}
|
||||
</Text>
|
||||
<Stack gap={2}>
|
||||
<div key={section.title}>
|
||||
<div className="fsb-section-label">{section.title}</div>
|
||||
<Stack gap={3}>
|
||||
{section.items.map((item) => renderTopLevelItem(item))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
</nav>
|
||||
|
||||
<div className="fsb-footer">
|
||||
<div className="fsb-status">
|
||||
<span className="fsb-pulse" />
|
||||
<Stack gap={0} style={{ minWidth: 0 }}>
|
||||
<Text size="xs" fw={600} style={{ color: "#166534", lineHeight: 1.3 }}>
|
||||
All systems operational
|
||||
</Text>
|
||||
<Text size="10px" style={{ color: "#94a3b8", lineHeight: 1.3 }}>
|
||||
EDR Platform · v1.0
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user