mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
style: update
This commit is contained in:
@@ -1,2 +1,33 @@
|
||||
@import "tailwindcss";
|
||||
@import "@edr/ui-common/theme.css" layer(theme);
|
||||
|
||||
/* Bridge the central Mantine theme into Tailwind. Mantine (createTheme) is the
|
||||
single source of truth; these just alias its generated CSS variables so
|
||||
`bg-edr-*`, `text-edr-*`, `border-edr-*` utilities resolve to the same tokens. */
|
||||
@theme {
|
||||
--color-edr-primary: var(--mantine-color-edr-green-5);
|
||||
--color-edr-primary-dark: var(--mantine-color-edr-green-7);
|
||||
--color-edr-bg: var(--mantine-color-edr-bg-6);
|
||||
--color-edr-card: var(--mantine-color-edr-card-6);
|
||||
--color-edr-border: var(--mantine-color-edr-border-6);
|
||||
--color-edr-divider: var(--mantine-color-edr-divider-6);
|
||||
--color-edr-text: var(--mantine-color-edr-text-6);
|
||||
--color-edr-muted: var(--mantine-color-edr-muted-6);
|
||||
--color-edr-soft: var(--mantine-color-edr-soft-6);
|
||||
--color-edr-ink: var(--mantine-color-edr-ink-6);
|
||||
--color-edr-accent: var(--mantine-color-edr-accent-6);
|
||||
|
||||
--color-edr-amber-soft: var(--mantine-color-edr-amber-soft-6);
|
||||
--color-edr-amber-text: var(--mantine-color-edr-amber-text-6);
|
||||
--color-edr-blue: var(--mantine-color-edr-blue-6);
|
||||
--color-edr-blue-soft: var(--mantine-color-edr-blue-soft-6);
|
||||
--color-edr-blue-dot: var(--mantine-color-edr-blue-dot-6);
|
||||
--color-edr-red: var(--mantine-color-edr-red-6);
|
||||
--color-edr-red-soft: var(--mantine-color-edr-red-soft-6);
|
||||
--color-edr-slate: var(--mantine-color-edr-slate-6);
|
||||
--color-edr-slate-soft: var(--mantine-color-edr-slate-soft-6);
|
||||
--color-edr-slate-soft2: var(--mantine-color-edr-slate-soft2-6);
|
||||
--color-edr-step: var(--mantine-color-edr-step-6);
|
||||
--color-edr-step-idle: var(--mantine-color-edr-step-idle-6);
|
||||
--color-edr-conn-idle: var(--mantine-color-edr-conn-idle-6);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&display=swap"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
|
||||
BIN
apps/edr-freight-web/portal/public/assets/edr-logo.png
Normal file
BIN
apps/edr-freight-web/portal/public/assets/edr-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
apps/edr-freight-web/portal/public/assets/train-edr.jpg
Normal file
BIN
apps/edr-freight-web/portal/public/assets/train-edr.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
@@ -1,58 +1,55 @@
|
||||
import {
|
||||
useNavigate,
|
||||
useLocation,
|
||||
Routes,
|
||||
Route,
|
||||
Outlet,
|
||||
} from "react-router-dom";
|
||||
import { AppLayout, type SidebarItem } from "@/components/AppLayout";
|
||||
import {
|
||||
CalendarCheck,
|
||||
MapPin,
|
||||
Receipt,
|
||||
Home,
|
||||
Loader2,
|
||||
User,
|
||||
MapPin,
|
||||
Receipt,
|
||||
Settings,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Outlet,
|
||||
Route,
|
||||
Routes,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
} from "react-router-dom";
|
||||
|
||||
import useAuth from "./hooks/useAuth";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import EDRFreightLandingPage from "./pages/EDRFreightLandingPage";
|
||||
import MyPortalPage from "./pages/MyPortalPage";
|
||||
import ProfilePage from "./pages/ProfilePage";
|
||||
import SettingsPage from "./pages/SettingsPage";
|
||||
import MyPortalPage from "./pages/MyPortalPage";
|
||||
import EDRFreightLandingPage from "./pages/EDRFreightLandingPage";
|
||||
import SignupPage from "./pages/accounts/SignupPage";
|
||||
import OnboardingPage from "./pages/accounts/OnboardingPage";
|
||||
import VerificationOtpPage from "./pages/accounts/VerificationOtpPage";
|
||||
import SetPasswordPage from "./pages/accounts/SetPasswordPage";
|
||||
import LoginPage from "./pages/accounts/LoginPage";
|
||||
import MyBookings from "./pages/bookings/MyBookings";
|
||||
import OnboardingPage from "./pages/accounts/OnboardingPage";
|
||||
import SetPasswordPage from "./pages/accounts/SetPasswordPage";
|
||||
import SignupPage from "./pages/accounts/SignupPage";
|
||||
import VerificationOtpPage from "./pages/accounts/VerificationOtpPage";
|
||||
import BillingPage from "./pages/billing/BillingPage";
|
||||
import BookingContractPage from "./pages/bookings/BookingContractPage";
|
||||
import BookingDetailPage from "./pages/bookings/BookingDetailPage";
|
||||
import NewBookingPage from "./pages/bookings/NewBookingPage";
|
||||
import EditBookingPage from "./pages/bookings/EditBookingPage";
|
||||
import MyBookings from "./pages/bookings/MyBookings";
|
||||
import NewBookingPage from "./pages/bookings/NewBookingPage";
|
||||
import CheckPaymentPage from "./pages/payments/CheckPaymentPage";
|
||||
import TrackingPage from "./pages/tracking/TrackingPage";
|
||||
import BillingPage from "./pages/billing/BillingPage";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const sidebarItems: SidebarItem[] = [
|
||||
{ section: "Overview", label: "Home", href: "/portal", icon: <Home size={18} /> },
|
||||
{ label: "Home", href: "/portal", icon: <Home size={18} /> },
|
||||
{
|
||||
section: "Operations",
|
||||
label: "My Bookings",
|
||||
href: "/bookings",
|
||||
icon: <CalendarCheck size={18} />,
|
||||
},
|
||||
{
|
||||
section: "Operations",
|
||||
label: "Tracking",
|
||||
href: "/tracking",
|
||||
icon: <MapPin size={18} />,
|
||||
},
|
||||
{
|
||||
section: "Operations",
|
||||
label: "Billing",
|
||||
href: "/billing",
|
||||
icon: <Receipt size={18} />,
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
>
|
||||
Ethio–Djibouti 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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +1,71 @@
|
||||
import { createTheme, type MantineColorsTuple } from "@mantine/core";
|
||||
import { colorsTuple, createTheme, type MantineColorsTuple } from "@mantine/core";
|
||||
|
||||
// Brand accent — used sparingly: primary actions, active nav, key highlights.
|
||||
const edrGreen: MantineColorsTuple = [
|
||||
"#ecfdf5",
|
||||
"#d1fae5",
|
||||
"#a7f3d0",
|
||||
"#6ee7b7",
|
||||
"#34d399",
|
||||
"#10b981",
|
||||
"#059669",
|
||||
"#047857",
|
||||
"#0EA371",
|
||||
"#0A8A5F",
|
||||
"#0A6F4D",
|
||||
"#065f46",
|
||||
"#064e3b",
|
||||
];
|
||||
|
||||
// Neutral gray ramp tuned for clean, low-contrast surfaces (Stripe/Notion feel).
|
||||
const neutral: MantineColorsTuple = [
|
||||
"#f8fafc",
|
||||
"#f1f5f9",
|
||||
"#e8edf2",
|
||||
"#dbe2ea",
|
||||
"#c2cbd6",
|
||||
"#9aa6b4",
|
||||
"#6b7785",
|
||||
"#4b5563",
|
||||
"#2f3742",
|
||||
"#1c2129",
|
||||
"#F4F7FA",
|
||||
"#eef2f7",
|
||||
"#E6ECF2",
|
||||
"#d0dae6",
|
||||
"#b0bfce",
|
||||
"#8fa0b2",
|
||||
"#6B7C8E",
|
||||
"#4b5a6a",
|
||||
"#10202F",
|
||||
"#0C1A2B",
|
||||
];
|
||||
|
||||
export const mantineTheme = createTheme({
|
||||
colors: {
|
||||
"edr-green": edrGreen,
|
||||
gray: neutral,
|
||||
|
||||
// Brand surface + text tokens (single-value semantic colors).
|
||||
// Each generates --mantine-color-{name}-{0..9} CSS variables.
|
||||
"edr-bg": colorsTuple("#F4F7FA"),
|
||||
"edr-card": colorsTuple("#FFFFFF"),
|
||||
"edr-border": colorsTuple("#E6ECF2"),
|
||||
"edr-divider": colorsTuple("#EEF1F5"),
|
||||
"edr-text": colorsTuple("#10202F"),
|
||||
"edr-muted": colorsTuple("#6B7C8E"),
|
||||
"edr-soft": colorsTuple("#ECF6F1"),
|
||||
"edr-ink": colorsTuple("#0C1A2B"),
|
||||
"edr-accent": colorsTuple("#F2A516"),
|
||||
|
||||
// Semantic status shades (from the design system).
|
||||
"edr-amber-soft": colorsTuple("#FDF3E0"),
|
||||
"edr-amber-text": colorsTuple("#9A5B00"),
|
||||
"edr-blue": colorsTuple("#2E5B96"),
|
||||
"edr-blue-soft": colorsTuple("#E9F0F8"),
|
||||
"edr-blue-dot": colorsTuple("#3B6FB0"),
|
||||
"edr-red": colorsTuple("#C0392B"),
|
||||
"edr-red-soft": colorsTuple("#FBEAE7"),
|
||||
"edr-slate": colorsTuple("#475569"),
|
||||
"edr-slate-soft": colorsTuple("#F1F4F7"),
|
||||
"edr-slate-soft2": colorsTuple("#EEF2F6"),
|
||||
"edr-step": colorsTuple("#94A3B8"),
|
||||
"edr-step-idle": colorsTuple("#D5DBE2"),
|
||||
"edr-conn-idle": colorsTuple("#E6EAEF"),
|
||||
},
|
||||
primaryColor: "edr-green",
|
||||
primaryShade: { light: 6, dark: 5 },
|
||||
primaryShade: { light: 5, dark: 4 },
|
||||
|
||||
white: "#ffffff",
|
||||
black: "#1c2129",
|
||||
black: "#10202F",
|
||||
|
||||
fontFamily:
|
||||
'"Plus Jakarta Sans",ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
'"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
|
||||
defaultRadius: "md",
|
||||
|
||||
@@ -49,7 +74,7 @@ export const mantineTheme = createTheme({
|
||||
sm: "6px",
|
||||
md: "8px",
|
||||
lg: "12px",
|
||||
xl: "16px",
|
||||
xl: "18px",
|
||||
},
|
||||
|
||||
spacing: {
|
||||
@@ -76,17 +101,13 @@ export const mantineTheme = createTheme({
|
||||
xl: "1.5",
|
||||
},
|
||||
|
||||
// Hierarchy comes from a strong, exponential size scale (~1.25 modular ratio),
|
||||
// not from heavy weights. Big steps at the top, calm weights throughout.
|
||||
// "Plus Jakarta Sans" gives headings a distinctive geometric character while
|
||||
// body text stays on the neutral system stack.
|
||||
headings: {
|
||||
fontFamily: '"Plus Jakarta Sans", var(--mantine-font-family)',
|
||||
fontWeight: "600",
|
||||
fontFamily: '"Inter", var(--mantine-font-family)',
|
||||
fontWeight: "700",
|
||||
sizes: {
|
||||
h1: { fontSize: "40px", lineHeight: "1.1", fontWeight: "800" },
|
||||
h2: { fontSize: "30px", lineHeight: "1.2", fontWeight: "700" },
|
||||
h3: { fontSize: "23px", lineHeight: "1.3", fontWeight: "600" },
|
||||
h1: { fontSize: "36px", lineHeight: "1.1", fontWeight: "800" },
|
||||
h2: { fontSize: "28px", lineHeight: "1.2", fontWeight: "700" },
|
||||
h3: { fontSize: "22px", lineHeight: "1.3", fontWeight: "600" },
|
||||
h4: { fontSize: "18px", lineHeight: "1.4", fontWeight: "600" },
|
||||
h5: { fontSize: "15px", lineHeight: "1.45", fontWeight: "600" },
|
||||
h6: { fontSize: "13px", lineHeight: "1.45", fontWeight: "600" },
|
||||
@@ -95,53 +116,39 @@ export const mantineTheme = createTheme({
|
||||
|
||||
shadows: {
|
||||
xs: "0 1px 2px rgba(16, 24, 40, 0.04)",
|
||||
sm: "0 1px 3px rgba(16, 24, 40, 0.06), 0 1px 2px rgba(16, 24, 40, 0.04)",
|
||||
sm: "0 1px 3px rgba(16, 24, 40, 0.06)",
|
||||
md: "0 4px 12px rgba(16, 24, 40, 0.06)",
|
||||
},
|
||||
|
||||
components: {
|
||||
Card: {
|
||||
defaultProps: {
|
||||
radius: "lg",
|
||||
radius: "xl",
|
||||
withBorder: true,
|
||||
shadow: "none",
|
||||
padding: "lg",
|
||||
},
|
||||
styles: {
|
||||
root: { borderColor: "#E6ECF2" },
|
||||
},
|
||||
},
|
||||
Button: {
|
||||
defaultProps: {
|
||||
radius: "md",
|
||||
},
|
||||
styles: {
|
||||
root: { fontWeight: 550 },
|
||||
},
|
||||
defaultProps: { radius: "md" },
|
||||
styles: { root: { fontWeight: 600 } },
|
||||
},
|
||||
Badge: {
|
||||
defaultProps: {
|
||||
radius: "sm",
|
||||
variant: "light",
|
||||
},
|
||||
styles: {
|
||||
root: { fontWeight: 550, textTransform: "none" },
|
||||
},
|
||||
defaultProps: { radius: "xl", variant: "light" },
|
||||
styles: { root: { fontWeight: 600, textTransform: "none" } },
|
||||
},
|
||||
Paper: {
|
||||
defaultProps: {
|
||||
radius: "lg",
|
||||
shadow: "none",
|
||||
withBorder: true,
|
||||
},
|
||||
defaultProps: { radius: "xl", shadow: "none", withBorder: true },
|
||||
styles: { root: { borderColor: "#E6ECF2" } },
|
||||
},
|
||||
Table: {
|
||||
defaultProps: {
|
||||
verticalSpacing: "sm",
|
||||
horizontalSpacing: "md",
|
||||
},
|
||||
defaultProps: { verticalSpacing: "sm", horizontalSpacing: "md" },
|
||||
},
|
||||
Title: {
|
||||
styles: {
|
||||
root: { letterSpacing: "-0.01em" },
|
||||
},
|
||||
styles: { root: { letterSpacing: "-0.01em" } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user