style: update

This commit is contained in:
Nathnael
2026-06-10 07:27:42 +00:00
parent 02621d05d8
commit f423fc2e6f
8 changed files with 852 additions and 790 deletions

View File

@@ -1,13 +1,9 @@
import { Fragment, type ReactNode, useState } from "react";
import {
ActionIcon,
AppShell,
Avatar,
Box,
Burger,
Divider,
Group,
Indicator,
Menu,
NavLink,
ScrollArea,
@@ -19,21 +15,20 @@ import { useDisclosure } from "@mantine/hooks";
import {
Bell,
ChevronDown,
ChevronRight,
Languages,
LogOut,
Menu as MenuIcon,
Moon,
Search,
Sun,
Train,
User,
} from "lucide-react";
import { type CSSProperties, Fragment, type ReactNode, useState } from "react";
export interface SidebarItem {
label: string;
href: string;
icon?: ReactNode;
children?: SidebarItem[];
/** Optional group heading; a small label is rendered when it changes. */
section?: string;
}
@@ -52,13 +47,23 @@ export interface AppLayoutProps {
type Theme = "light" | "dark";
const THEME_KEY = "edr-theme";
const EDR = {
primary: "#0EA371",
primaryDark: "#0A6F4D",
soft: "#ECF6F1",
border: "#E6ECF2",
bg: "#F4F7FA",
text: "#10202F",
muted: "#6B7C8E",
ink: "#0C1A2B",
accent: "#F2A516",
};
function getStoredTheme(): Theme {
if (typeof window === "undefined") return "light";
const stored = localStorage.getItem(THEME_KEY);
if (stored === "dark" || stored === "light") return stored;
return window.matchMedia?.("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
function getInitials(name: string): string {
@@ -70,23 +75,15 @@ function getInitials(name: string): string {
.join("");
}
function getActivePage(
items: SidebarItem[],
activePath: string,
): { label: string } | null {
function getActivePage(items: SidebarItem[], activePath: string): { label: string } | null {
const path = activePath.toLowerCase();
for (const item of items) {
if (
path === item.href.toLowerCase() ||
path.startsWith(item.href.toLowerCase() + "/")
) {
if (path === item.href.toLowerCase() || path.startsWith(item.href.toLowerCase() + "/")) {
return { label: item.label };
}
if (item.children) {
const childMatch = item.children.find(
(c) =>
path === c.href.toLowerCase() ||
path.startsWith(c.href.toLowerCase() + "/"),
(c) => path === c.href.toLowerCase() || path.startsWith(c.href.toLowerCase() + "/"),
);
if (childMatch) return { label: childMatch.label };
}
@@ -94,22 +91,18 @@ function getActivePage(
return null;
}
// NavLink classNames for the dark sidebar — Tailwind utilities (with v4 `!`
// important suffix) override Mantine's default active styling.
const navClassNames = (active: boolean) => {
const base =
"rounded-[10px] font-medium transition-all duration-150 active:scale-[0.98]";
if (active) {
return {
root: `${base} bg-gradient-to-br! from-emerald-600! to-emerald-500! text-white! shadow-[0_2px_8px_-4px_rgba(16,185,129,0.45)]`,
label: "text-white!",
section: "text-white!",
root: `rounded-[10px] font-medium transition-all duration-150 bg-[#ECF6F1]! ring-1 ring-inset ring-[#0EA371]/15`,
label: `text-[#0A6F4D]! font-bold!`,
section: `text-[#0A6F4D]!`,
};
}
return {
root: `${base} text-white/60! hover:bg-white/[0.07]! hover:text-white!`,
label: "text-inherit!",
section: "text-inherit! opacity-90",
root: `rounded-[10px] font-medium transition-all duration-150 hover:bg-[#F1F4F7]!`,
label: `text-[#3D4D5C]! font-semibold! hover:text-[#0C1A2B]!`,
section: `text-[#54657A]! hover:text-[#0C1A2B]!`,
};
};
@@ -146,153 +139,142 @@ export function AppLayout({
activePath === item.href.toLowerCase() ||
activePath.startsWith(item.href.toLowerCase() + "/");
// Shared header "island" styles — every control is a consistent 36px chip.
const islandStyle: CSSProperties = {
width: 36,
height: 36,
borderRadius: 999,
border: `1px solid ${EDR.border}`,
backgroundColor: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
cursor: "pointer",
};
const toggleStyle: CSSProperties = { ...islandStyle, borderRadius: 10 };
return (
<AppShell
layout="alt"
navbar={{
width: 264,
breakpoint: "sm",
collapsed: { mobile: !mobileOpen },
}}
header={{ height: 60 }}
navbar={{ width: 260, breakpoint: "sm", collapsed: { mobile: !mobileOpen } }}
header={{ height: 56 }}
padding={0}
>
{/* ── Header ──────────────────────────────────────────────────────────── */}
{/* ── Header (frosted glass; compact floating islands, mirrors the pen) ── */}
<AppShell.Header
withBorder={false}
className="border-b border-[var(--mantine-color-gray-2)]! bg-white/70! backdrop-blur-md backdrop-saturate-150"
style={{
backdropFilter: "blur(14px)",
WebkitBackdropFilter: "blur(14px)",
border: "none",
boxShadow: "none",
background:'transparent',
}}
>
<Group h="100%" px="lg" justify="space-between">
<Group gap="sm">
<Burger
opened={mobileOpen}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Group gap={6} wrap="nowrap">
<Text
size="sm"
className="font-medium text-[var(--mantine-color-gray-5)]"
visibleFrom="xs"
>
{title}
</Text>
<Box visibleFrom="xs" className="flex">
<ChevronRight size={14} color="var(--mantine-color-gray-4)" />
</Box>
<Text
size="md"
className="font-semibold tracking-tight text-[var(--mantine-color-gray-8)]"
>
{activePage ? activePage.label : title}
</Text>
</Group>
<Group h="100%" px={24} justify="space-between" wrap="nowrap">
{/* Left: sidebar toggle + page title */}
<Group gap={12} wrap="nowrap" align="center">
<UnstyledButton onClick={toggleMobile} style={toggleStyle} aria-label="Toggle sidebar">
<MenuIcon size={18} color={EDR.text} strokeWidth={1.8} />
</UnstyledButton>
<Text
style={{
fontFamily: '"Inter", sans-serif',
fontSize: 16,
fontWeight: 700,
color: EDR.text,
lineHeight: 1,
}}
>
{activePage ? activePage.label : title}
</Text>
</Group>
{/* Right: utility actions + user menu */}
<Group gap={4}>
<ActionIcon
variant="subtle"
color="gray"
size="lg"
radius="md"
className="transition-transform hover:-translate-y-px"
aria-label="Change language"
{/* Right: search + bell + avatar */}
<Group gap={10} wrap="nowrap" align="center">
{/* Search pill */}
<Group
gap={8}
align="center"
visibleFrom="sm"
style={{
width: 260,
height: 36,
borderRadius: 999,
border: `1px solid ${EDR.border}`,
backgroundColor: "#fff",
padding: "0 14px",
cursor: "text",
}}
>
<Languages size={18} />
</ActionIcon>
<Search size={15} color={EDR.muted} strokeWidth={1.8} />
<Text size="sm" style={{ color: EDR.muted, userSelect: "none" }}>
Search shipments, bookings
</Text>
</Group>
<Indicator color="red" size={7} offset={6} zIndex={10} withBorder>
<ActionIcon
variant="subtle"
color="gray"
size="lg"
radius="md"
className="transition-transform hover:-translate-y-px"
aria-label="Notifications"
>
<Bell size={18} />
</ActionIcon>
</Indicator>
{/* Bell */}
<Box style={{ position: "relative" }}>
<UnstyledButton style={islandStyle} aria-label="Notifications">
<Bell size={17} color={EDR.text} strokeWidth={1.8} />
</UnstyledButton>
<Box
style={{
position: "absolute",
top: 7,
right: 7,
width: 7,
height: 7,
borderRadius: "50%",
backgroundColor: EDR.accent,
border: "1.5px solid #fff",
pointerEvents: "none",
}}
/>
</Box>
{enableThemeToggle && (
<ActionIcon
variant="subtle"
color="gray"
size="lg"
radius="md"
className="transition-transform hover:-translate-y-px"
onClick={toggleTheme}
aria-label="Toggle theme"
>
{theme === "dark" ? <Sun size={18} /> : <Moon size={18} />}
</ActionIcon>
<UnstyledButton onClick={toggleTheme} style={islandStyle} aria-label="Toggle theme">
{theme === "dark"
? <Sun size={17} color={EDR.text} strokeWidth={1.8} />
: <Moon size={17} color={EDR.text} strokeWidth={1.8} />}
</UnstyledButton>
)}
<Divider orientation="vertical" my={14} mx={6} />
<Menu
width={232}
position="bottom-end"
withinPortal
shadow="md"
offset={8}
radius="md"
>
{/* Avatar pill */}
<Menu width={220} position="bottom-end" withinPortal shadow="md" offset={8} radius="md">
<Menu.Target>
<UnstyledButton
px={6}
py={4}
className="rounded-[10px] transition-colors hover:bg-[var(--mantine-color-gray-1)]"
style={{
height: 36,
borderRadius: 999,
border: `1px solid ${EDR.border}`,
backgroundColor: "#fff",
padding: "0 10px 0 4px",
display: "flex",
alignItems: "center",
gap: 7,
cursor: "pointer",
}}
>
<Group gap={8} wrap="nowrap">
<Avatar
radius="xl"
size={32}
gradient={{ from: "edr-green.5", to: "edr-green.8", deg: 135 }}
variant="gradient"
>
<Text fw={600} fz={11} c="white">
{initials}
</Text>
</Avatar>
<Box visibleFrom="sm" className="min-w-0">
<Text size="sm" fw={600} maw={120} truncate lh={1.2}>
{userName}
</Text>
<Text size="xs" c="dimmed" lh={1.2}>
Customer
</Text>
</Box>
<ChevronDown size={14} color="var(--mantine-color-gray-5)" />
</Group>
<Avatar radius="xl" size={28} color="edr-green.5">
<Text fw={700} fz={12} c="white">{initials}</Text>
</Avatar>
<ChevronDown size={15} color={EDR.muted} strokeWidth={1.8} />
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>
<Box px="sm" py="xs">
<Text size="sm" fw={600} truncate>
{userName}
</Text>
{userEmail && (
<Text size="xs" c="dimmed" truncate>
{userEmail}
</Text>
)}
<Text size="sm" fw={600} truncate style={{ color: EDR.text }}>{userName}</Text>
{userEmail && <Text size="xs" c="dimmed" truncate>{userEmail}</Text>}
</Box>
<Divider />
<Menu.Item
leftSection={<User size={15} />}
onClick={() => navigate("/profile")}
>
<Menu.Item leftSection={<User size={15} />} onClick={() => navigate("/profile")}>
Profile
</Menu.Item>
<Menu.Item
leftSection={<LogOut size={15} />}
color="red"
onClick={onLogout}
>
<Menu.Item leftSection={<LogOut size={15} />} color="red" onClick={onLogout}>
Logout
</Menu.Item>
</Menu.Dropdown>
@@ -301,38 +283,67 @@ export function AppLayout({
</Group>
</AppShell.Header>
{/* ── Sidebar ─────────────────────────────────────────────────────────── */}
{/* ── Sidebar ── */}
<AppShell.Navbar
withBorder={false}
className="flex flex-col border-r border-white/[0.06]! bg-gradient-to-b from-[#1a2230] to-[#141a26]"
style={{
backgroundColor: "#ffffff",
borderRight: `1px solid ${EDR.border}`,
display: "flex",
flexDirection: "column",
}}
>
{/* Brand */}
<Box className="flex h-[60px] flex-shrink-0 items-center border-b border-white/[0.07] px-4">
<Group gap="sm" wrap="nowrap">
<Box className="flex h-[34px] w-[34px] flex-shrink-0 items-center justify-center rounded-[10px] bg-gradient-to-br from-emerald-500 to-emerald-700 shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]">
<Train size={18} color="white" strokeWidth={2.2} />
</Box>
<Box
style={{
height: 60,
flexShrink: 0,
display: "flex",
alignItems: "center",
padding: "0 18px",
}}
>
<Group gap={11} wrap="nowrap">
<img
src="/assets/edr-logo.png"
alt="EDR"
style={{ width: 40, height: 40, objectFit: "contain", flexShrink: 0 }}
/>
<Box>
<Text className="font-bold leading-tight tracking-tight text-white" size="md">
{title}
<Text
style={{
fontFamily: '"Inter", sans-serif',
fontWeight: 800,
fontSize: 19,
letterSpacing: "0.03em",
color: EDR.primary,
lineHeight: 1.15,
}}
>
EDR FREIGHT
</Text>
<Text className="text-[10px] font-semibold uppercase tracking-[0.12em] text-white/40">
Logistics Portal
<Text
style={{
fontSize: 10.5,
fontWeight: 500,
color: EDR.muted,
lineHeight: 1.2,
}}
>
EthioDjibouti Railway
</Text>
</Box>
</Group>
</Box>
{/* Nav links */}
{/* Nav */}
<ScrollArea flex={1} type="never" p="sm">
<Stack gap={3}>
{sidebarItems.map((item, i) => {
const active = isItemActive(item);
const hasChildren = !!item.children?.length;
const childActive =
item.children?.some((c) =>
activePath.startsWith(c.href.toLowerCase()),
) ?? false;
item.children?.some((c) => activePath.startsWith(c.href.toLowerCase())) ?? false;
const prevSection = sidebarItems[i - 1]?.section;
const sectionLabel =
@@ -343,8 +354,13 @@ export function AppLayout({
tt="uppercase"
px="sm"
mt={i === 0 ? 4 : "md"}
mb={6}
className="font-bold tracking-[0.1em] text-white/30"
mb={4}
style={{
fontWeight: 600,
letterSpacing: "0.08em",
color: EDR.muted,
fontSize: 11,
}}
>
{item.section}
</Text>
@@ -394,35 +410,118 @@ export function AppLayout({
</Stack>
</ScrollArea>
{/* Bottom user */}
<Box className="m-3 flex-shrink-0 rounded-xl border border-white/[0.07] bg-white/[0.04] p-2.5">
<Group gap="sm" wrap="nowrap">
{/* Promo card */}
<Box style={{ padding: "0 12px 12px" }}>
<Box
style={{
borderRadius: 16,
overflow: "hidden",
position: "relative",
height: 260,
marginBottom: 10,
}}
>
{/* Train image fills the full card */}
<img
src="/assets/train-edr.jpg"
alt=""
style={{
position: "absolute",
bottom:"-16px",
left:0,
right:"-70px",
width: "120%",
objectFit: "cover",
}}
/>
{/* Diagonal green overlay: lower-left → upper-right cut */}
<Box
style={{
position: "absolute",
inset: 0,
top:"-75%",
background: "linear-gradient(165deg, #006149 40%, #0DC9A4 70%, #0DC9A402 80%)",
clipPath: "polygon(0 0, 100% 0, 100% 58%, 0 72%)",
}}
/>
{/* Text sits on top of the green overlay */}
<Box style={{ position: "relative", zIndex: 1, padding: "16px 16px 0" }} className="max-w-[180px]">
<Text style={{ fontSize: 15, fontWeight: 800, color: "#fff", lineHeight: 1.3, marginBottom: 4 }}>
Moving Africa Forward
</Text>
<Text style={{ fontSize: 11, color: "rgba(255,255,255,0.7)", lineHeight: 1.5 }}>
Reliable. Efficient. Connected.
</Text>
</Box>
{/* Learn More — visible on the image area */}
<Box
style={{
position: "absolute",
bottom: 10,
right: 10,
zIndex: 2,
display: "inline-flex",
alignItems: "center",
gap: 5,
backgroundColor: "#fff",
borderRadius: 8,
padding: "6px 12px",
cursor: "pointer",
}}
>
<Text style={{ fontSize: 12, fontWeight: 600, color: EDR.primaryDark }}>Learn More</Text>
<span style={{ fontSize: 12, color: EDR.primaryDark }}></span>
</Box>
</Box>
{/* Profile */}
<Box
style={{
borderRadius: 12,
border: `1px solid ${EDR.border}`,
padding: "10px",
display: "flex",
alignItems: "center",
gap: 10,
cursor: "pointer",
}}
>
<Avatar
radius="xl"
size={34}
gradient={{ from: "edr-green.5", to: "edr-green.8", deg: 135 }}
variant="gradient"
size={38}
color="edr-green.5"
style={{ flexShrink: 0 }}
>
<Text fw={600} fz={12} c="white">
{initials}
</Text>
<Text fw={600} fz={13} c="white">{initials}</Text>
</Avatar>
<Box className="min-w-0">
<Text size="sm" fw={600} truncate className="text-white">
<Box style={{ minWidth: 0, flex: 1 }}>
<Text size="sm" fw={600} truncate style={{ color: EDR.text, lineHeight: 1.3 }}>
{userName}
</Text>
{userEmail && (
<Text size="xs" truncate className="text-white/40">
<Text size="xs" truncate style={{ color: EDR.muted, lineHeight: 1.3 }}>
{userEmail}
</Text>
)}
</Box>
</Group>
<ChevronDown size={16} color={EDR.muted} style={{ flexShrink: 0 }} />
</Box>
</Box>
</AppShell.Navbar>
{/* ── Main ────────────────────────────────────────────────────────────── */}
<AppShell.Main className="bg-[var(--mantine-color-gray-0)]">
{/* ── Main ── */}
<AppShell.Main
style={{
backgroundColor: EDR.bg,
backgroundImage:
"radial-gradient(58% 42% at 100% 0%, rgba(14,163,113,0.16) 0%, rgba(14,163,113,0.06) 38%, rgba(14,163,113,0) 72%)",
backgroundRepeat: "no-repeat",
backgroundAttachment: "fixed",
}}
>
{children}
</AppShell.Main>
</AppShell>