style:wip dashboard ui revamp

This commit is contained in:
Nathnael
2026-06-20 10:20:38 +00:00
parent e4955c75de
commit 138ac5b47d
10 changed files with 647 additions and 669 deletions

View File

@@ -47,7 +47,6 @@ const ISLAND =
"flex size-9 shrink-0 items-center justify-center rounded-full border border-edr-border bg-white text-edr-text transition-colors hover:bg-[#F1F4F7]";
const FreightDashboardHeader = ({
pageMeta,
headerRight,
enableThemeToggle = false,
userName = "User",
@@ -86,7 +85,8 @@ const FreightDashboardHeader = ({
}}
>
<Group h="100%" px={20} justify="space-between" wrap="nowrap">
{/* Left: burger (mobile) + page title */}
{/* Left: burger (mobile) + search — the search now occupies the slot
the page title used to hold; each page owns its own title. */}
<Group gap={12} wrap="nowrap" align="center" style={{ minWidth: 0 }}>
<Burger
opened={mobileOpened}
@@ -95,27 +95,22 @@ const FreightDashboardHeader = ({
size="sm"
aria-label="Toggle sidebar"
/>
<Text fw={700} truncate className="text-edr-text!" fz={17} style={{ letterSpacing: "-0.02em", lineHeight: 1.2 }}>
{pageMeta.title}
</Text>
</Group>
{/* Right: search + actions + avatar */}
<Group gap={10} wrap="nowrap" align="center">
{/* Search pill */}
<Group
gap={8}
align="center"
visibleFrom="md"
visibleFrom="sm"
className="h-9 cursor-text rounded-full border border-edr-border bg-white px-3.5"
style={{ width: 260 }}
style={{ width: 280 }}
>
<Search size={15} className="text-edr-muted" strokeWidth={1.8} />
<Text size="sm" className="select-none text-edr-muted!">
Search bookings, trains
</Text>
</Group>
</Group>
{/* Right: actions + avatar */}
<Group gap={10} wrap="nowrap" align="center">
<Tooltip label="Language" withArrow openDelay={300}>
<UnstyledButton className={ISLAND} aria-label="Language">
<Languages size={17} strokeWidth={1.8} />

View File

@@ -50,6 +50,27 @@ const ROUTE_META: Array<{ prefix: string; meta: PageMeta }> = [
subtitle: "View booking payment transactions",
},
},
{
prefix: "/dashboard/warehouse-dashboard",
meta: {
title: "Warehouse Dashboard",
subtitle: "Live overview of warehouse capacity and inventory lifecycle",
},
},
{
prefix: "/dashboard/warehouses/",
meta: {
title: "Warehouse detail",
subtitle: "Yards, zones, and inventory for this warehouse",
},
},
{
prefix: "/dashboard/warehouses",
meta: {
title: "Warehouses",
subtitle: "Manage warehouses, yards and zones",
},
},
{
prefix: "/dashboard/operations/train-scheduling-v2/",
meta: {

View File

@@ -0,0 +1,94 @@
import { Card, Skeleton, Text } from "@mantine/core";
import type { LucideIcon } from "lucide-react";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export interface KpiItem {
label: string;
value: ReactNode;
/** Optional leading icon rendered in a tinted chip. */
icon?: LucideIcon;
/** Secondary line under the label (e.g. a unit or comparison). */
hint?: string;
/**
* Mantine color name for the icon chip (e.g. "edr-green", "red", "yellow").
* Defaults to the brand green so a strip reads as uniform unless a page opts
* into semantic tints.
*/
color?: string;
}
export interface KpiStripProps {
items: KpiItem[];
/** Show skeletons in place of values while data loads. */
loading?: boolean;
}
/**
* A single bordered card divided into up to five KPI cells:
* `[ kpi | kpi | kpi ]`. Hairline dividers separate cells (vertical on wide
* screens, horizontal when they wrap). Surface, border and shadow all come from
* the theme — no per-cell backgrounds, gradients or custom shadows.
*/
export function KpiStrip({ items, loading = false }: KpiStripProps) {
// The spec caps a strip at five cells; extra items are dropped rather than
// silently overflowing into an unreadable row.
const cells = items.slice(0, 5);
return (
<Card withBorder shadow="sm" radius="lg" p={0} className="overflow-hidden">
<div className="flex flex-col sm:flex-row">
{cells.map((item, index) => {
const Icon = item.icon;
const color = item.color ?? "edr-green";
return (
<div
key={item.label}
className={cn(
"flex flex-1 items-center gap-3 px-5 py-4",
index > 0 &&
"border-t border-edr-border sm:border-l sm:border-t-0",
)}
>
{Icon ? (
<div
className="flex size-10 shrink-0 items-center justify-center rounded-lg"
style={{
background: `var(--mantine-color-${color}-1)`,
color: `var(--mantine-color-${color}-7)`,
}}
>
<Icon size={20} strokeWidth={2} />
</div>
) : null}
<div style={{ minWidth: 0 }}>
{loading ? (
<Skeleton height={26} width={72} radius="sm" my={2} />
) : (
<Text
fw={800}
fz={24}
lh={1.05}
c="edr-text"
style={{ letterSpacing: "-0.02em" }}
truncate
>
{item.value}
</Text>
)}
<Text size="xs" fw={600} c="edr-muted" truncate>
{item.label}
{item.hint ? ` · ${item.hint}` : ""}
</Text>
</div>
</div>
);
})}
</div>
</Card>
);
}
export default KpiStrip;

View File

@@ -0,0 +1,26 @@
import { Box, Stack, type MantineSpacing } from "@mantine/core";
import type { ReactNode } from "react";
export interface PageContainerProps {
children: ReactNode;
/** Drop the max-width cap for full-bleed pages (boards, very wide tables). */
fluid?: boolean;
/** Vertical gap between the page's stacked sections. */
gap?: MantineSpacing;
}
/**
* Standard page shell: one consistent inset + a vertical Stack so every
* dashboard page shares the same outer padding and inter-section rhythm.
* The surrounding AppShell.Main already paints the page background, so this
* never sets its own — pages stay on the shared `edr-bg` surface.
*/
export function PageContainer({ children, fluid = false, gap = "lg" }: PageContainerProps) {
return (
<Box px="lg" py="lg" mx="auto" w="100%" maw={fluid ? undefined : 1600}>
<Stack gap={gap}>{children}</Stack>
</Box>
);
}
export default PageContainer;

View File

@@ -0,0 +1,78 @@
import { ActionIcon, Group, Stack, Text, Title } from "@mantine/core";
import { ArrowLeft } from "lucide-react";
import type { ReactNode } from "react";
import { useNavigate } from "react-router-dom";
import Breadcrumbs, { type BreadcrumbItem } from "@/components/ui/Breadcrumbs";
export interface PageHeaderProps {
title: string;
subtitle?: string;
/** Breadcrumb trail — pass only on nested pages (details, sub-resources). */
breadcrumbs?: BreadcrumbItem[];
/** Route to return to; renders a back arrow before the title. */
backTo?: string;
/** Inline content beside the title (e.g. status badges). */
meta?: ReactNode;
/** Right-aligned actions — the primary CTA lives here. */
action?: ReactNode;
}
/**
* Unified page header: optional breadcrumbs, a title (with optional back arrow
* and inline meta), a subtitle, and a right-aligned action slot. Keeps title /
* action placement and spacing identical across every dashboard page.
*/
export function PageHeader({
title,
subtitle,
breadcrumbs,
backTo,
meta,
action,
}: PageHeaderProps) {
const navigate = useNavigate();
return (
<Stack gap="sm">
{breadcrumbs?.length ? <Breadcrumbs items={breadcrumbs} /> : null}
<Group justify="space-between" align="flex-start" gap="md">
<Group gap="sm" align="center" wrap="nowrap" style={{ minWidth: 0 }}>
{backTo ? (
<ActionIcon
variant="subtle"
color="gray"
onClick={() => navigate(backTo)}
aria-label="Go back"
>
<ArrowLeft size={18} />
</ActionIcon>
) : null}
<div style={{ minWidth: 0 }}>
<Group gap="sm" align="center" wrap="nowrap">
<Title order={2} className="truncate">
{title}
</Title>
{meta}
</Group>
{subtitle ? (
<Text c="dimmed" size="sm" mt={4}>
{subtitle}
</Text>
) : null}
</div>
</Group>
{action ? (
<Group gap="sm" wrap="nowrap">
{action}
</Group>
) : null}
</Group>
</Stack>
);
}
export default PageHeader;

View File

@@ -0,0 +1,6 @@
export { PageContainer } from "./PageContainer";
export type { PageContainerProps } from "./PageContainer";
export { PageHeader } from "./PageHeader";
export type { PageHeaderProps } from "./PageHeader";
export { KpiStrip } from "./KpiStrip";
export type { KpiItem, KpiStripProps } from "./KpiStrip";