mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 21:15:41 +00:00
style:wip dashboard ui revamp
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user