Files
edr-platform/apps/edr-freight-web/backoffice/src/components/trainScheduling/scheduleVisuals.tsx
2026-06-09 23:25:52 +00:00

253 lines
6.7 KiB
TypeScript

import type { ReactNode } from "react";
import { Box, Group, Paper, Stack, Text } from "@mantine/core";
import type { LucideIcon } from "lucide-react";
import { MapPin } from "lucide-react";
import { freightBrand } from "@/theme/freight-brand";
/**
* Shared visual building blocks for the Train Scheduling V2 surfaces.
* Everything keys off the freight brand green so the list + detail pages
* read as one cohesive, premium product.
*/
export const scheduleBrand = {
/** Deep green → emerald hero wash used across scheduling surfaces. */
heroGradient: `linear-gradient(135deg, ${freightBrand.primaryDark} 0%, ${freightBrand.primary} 48%, ${freightBrand.primaryLight} 120%)`,
/** Soft tinted surface for cards on white backgrounds. */
softSurface: `linear-gradient(135deg, ${freightBrand.mutedBg} 0%, #ffffff 60%, var(--mantine-color-gray-0) 100%)`,
ring: freightBrand.ring,
shadow: freightBrand.shadow,
shadowSm: freightBrand.shadowSm,
mutedBorder: freightBrand.mutedBorder,
} as const;
const STATUS_META: Record<
string,
{ color: string; dot: string; label?: string }
> = {
DRAFT: { color: "gray", dot: "var(--mantine-color-gray-5)" },
SCHEDULED: { color: "green", dot: freightBrand.primary },
DISPATCHED: { color: "teal", dot: "var(--mantine-color-teal-6)" },
ARRIVED: { color: "blue", dot: "var(--mantine-color-blue-6)" },
CANCELLED: { color: "red", dot: "var(--mantine-color-red-6)" },
};
export function statusMeta(status: string) {
return STATUS_META[status] ?? STATUS_META.DRAFT;
}
/**
* Status pill with a leading status dot — clearer at a glance than a plain
* badge and consistent everywhere a schedule status appears.
*/
export function StatusPill({
status,
size = "sm",
}: {
status: string;
size?: "sm" | "md";
}) {
const meta = statusMeta(status);
const isMd = size === "md";
return (
<Group
gap={isMd ? 8 : 6}
wrap="nowrap"
style={{
display: "inline-flex",
padding: isMd ? "5px 12px" : "3px 10px",
borderRadius: 999,
background: `var(--mantine-color-${meta.color}-0)`,
border: `1px solid var(--mantine-color-${meta.color}-2)`,
}}
>
<Box
w={isMd ? 8 : 7}
h={isMd ? 8 : 7}
style={{
borderRadius: 999,
background: meta.dot,
boxShadow: `0 0 0 3px var(--mantine-color-${meta.color}-1)`,
flexShrink: 0,
}}
/>
<Text
size={isMd ? "sm" : "xs"}
fw={600}
c={`${meta.color}.8`}
style={{ letterSpacing: 0.2, lineHeight: 1 }}
>
{meta.label ?? status}
</Text>
</Group>
);
}
/**
* Compact metric tile used in hero strips. `onDark` flips colors for use on
* the green hero gradient.
*/
export function StatTile({
icon: Icon,
label,
value,
hint,
onDark = false,
accent = freightBrand.primary,
}: {
icon?: LucideIcon;
label: string;
value: ReactNode;
hint?: ReactNode;
onDark?: boolean;
accent?: string;
}) {
return (
<Paper
p="md"
radius="lg"
style={
onDark
? {
background: "rgba(255,255,255,0.12)",
border: "1px solid rgba(255,255,255,0.18)",
backdropFilter: "blur(6px)",
}
: {
background: "white",
border: "1px solid var(--mantine-color-gray-2)",
}
}
>
<Group gap="sm" wrap="nowrap" align="flex-start">
{Icon ? (
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 36,
height: 36,
borderRadius: 10,
flexShrink: 0,
background: onDark ? "rgba(255,255,255,0.16)" : `${accent}1a`,
color: onDark ? "white" : accent,
}}
>
<Icon size={18} />
</Box>
) : null}
<Stack gap={2} style={{ minWidth: 0 }}>
<Text
size="xs"
fw={600}
tt="uppercase"
style={{ letterSpacing: 0.4 }}
c={onDark ? "rgba(255,255,255,0.75)" : "dimmed"}
>
{label}
</Text>
<Text
fw={700}
size="lg"
lh={1.1}
c={onDark ? "white" : undefined}
style={{ whiteSpace: "nowrap" }}
>
{value}
</Text>
{hint ? (
<Text size="xs" c={onDark ? "rgba(255,255,255,0.7)" : "dimmed"}>
{hint}
</Text>
) : null}
</Stack>
</Group>
</Paper>
);
}
/**
* Origin → destination corridor visual: two anchored stops joined by a rail
* line. `variant="compact"` is for dense table rows; `default` for cards.
*/
export function RouteCorridor({
origin,
destination,
variant = "default",
onDark = false,
}: {
origin?: string | null;
destination?: string | null;
variant?: "default" | "compact";
onDark?: boolean;
}) {
const compact = variant === "compact";
const dim = onDark ? "rgba(255,255,255,0.7)" : "var(--mantine-color-gray-5)";
const strong = onDark ? "white" : "var(--mantine-color-gray-8)";
const lineColor = onDark ? "rgba(255,255,255,0.4)" : "var(--mantine-color-gray-3)";
const accent = onDark ? "white" : freightBrand.primary;
return (
<Group gap={compact ? 6 : 8} wrap="nowrap" align="center" style={{ minWidth: 0 }}>
<Box
w={compact ? 7 : 9}
h={compact ? 7 : 9}
style={{
borderRadius: 999,
flexShrink: 0,
border: `2px solid ${accent}`,
background: onDark ? "transparent" : "white",
}}
/>
<Text
size={compact ? "sm" : "sm"}
fw={600}
c={strong}
style={{ whiteSpace: "nowrap" }}
>
{origin ?? "—"}
</Text>
<Box
style={{
flex: 1,
minWidth: compact ? 16 : 24,
height: 0,
borderTop: `2px dashed ${lineColor}`,
position: "relative",
}}
>
<MapPin
size={compact ? 11 : 13}
color={dim}
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
background: onDark ? "transparent" : "white",
}}
/>
</Box>
<Text
size={compact ? "sm" : "sm"}
fw={600}
c={strong}
style={{ whiteSpace: "nowrap" }}
>
{destination ?? "—"}
</Text>
<Box
w={compact ? 7 : 9}
h={compact ? 7 : 9}
style={{
borderRadius: 999,
flexShrink: 0,
background: accent,
}}
/>
</Group>
);
}