mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
ui design for schedule
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user