mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 20:10:56 +00:00
221 lines
6.1 KiB
TypeScript
221 lines
6.1 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { Badge, Box, Collapse, Group, Stack, Text, UnstyledButton } from "@mantine/core";
|
|
import type { LucideIcon } from "lucide-react";
|
|
import { Check, ChevronDown, Lock } from "lucide-react";
|
|
|
|
import { freightBrand } from "@/theme/freight-brand";
|
|
|
|
export type WorkflowStepState = "complete" | "active" | "upcoming";
|
|
|
|
/**
|
|
* A single collapsible step in the scheduling workflow. Steps are stacked
|
|
* inside <WorkflowRail> which paints the continuous connector behind the
|
|
* status circles.
|
|
*/
|
|
export function WorkflowStep({
|
|
index,
|
|
icon: Icon,
|
|
title,
|
|
subtitle,
|
|
state,
|
|
open,
|
|
onToggle,
|
|
rightSlot,
|
|
locked = false,
|
|
children,
|
|
}: {
|
|
index: number;
|
|
icon: LucideIcon;
|
|
title: string;
|
|
subtitle?: string;
|
|
state: WorkflowStepState;
|
|
open: boolean;
|
|
onToggle: () => void;
|
|
rightSlot?: ReactNode;
|
|
locked?: boolean;
|
|
children: ReactNode;
|
|
}) {
|
|
const isComplete = state === "complete";
|
|
const isActive = state === "active";
|
|
|
|
const circle = (() => {
|
|
if (isComplete) {
|
|
return {
|
|
bg: freightBrand.primary,
|
|
color: "white",
|
|
border: freightBrand.primary,
|
|
shadow: `0 4px 10px ${freightBrand.ring}`,
|
|
};
|
|
}
|
|
if (isActive) {
|
|
return {
|
|
bg: "white",
|
|
color: freightBrand.primary,
|
|
border: freightBrand.primary,
|
|
shadow: `0 0 0 4px ${freightBrand.ring}`,
|
|
};
|
|
}
|
|
return {
|
|
bg: "var(--mantine-color-gray-1)",
|
|
color: "var(--mantine-color-gray-5)",
|
|
border: "var(--mantine-color-gray-3)",
|
|
shadow: "none",
|
|
};
|
|
})();
|
|
|
|
return (
|
|
<Group gap="md" align="stretch" wrap="nowrap">
|
|
{/* Status circle (sits above the rail) */}
|
|
<Box
|
|
style={{
|
|
width: 40,
|
|
flexShrink: 0,
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
<Box
|
|
style={{
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: "50%",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
background: circle.bg,
|
|
color: circle.color,
|
|
border: `2px solid ${circle.border}`,
|
|
boxShadow: circle.shadow,
|
|
fontWeight: 700,
|
|
fontSize: 15,
|
|
transition: "all 150ms ease",
|
|
position: "relative",
|
|
zIndex: 1,
|
|
}}
|
|
>
|
|
{isComplete ? <Check size={20} /> : <Icon size={18} />}
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Step card */}
|
|
<Box
|
|
style={{
|
|
flex: 1,
|
|
minWidth: 0,
|
|
marginBottom: 4,
|
|
borderRadius: 16,
|
|
border: `1px solid ${
|
|
isActive ? freightBrand.mutedBorder : "var(--mantine-color-gray-2)"
|
|
}`,
|
|
background: isActive ? freightBrand.mutedBg : "white",
|
|
boxShadow: isActive ? `0 6px 20px ${freightBrand.ring}` : "none",
|
|
overflow: "hidden",
|
|
transition: "all 150ms ease",
|
|
}}
|
|
>
|
|
<UnstyledButton
|
|
onClick={onToggle}
|
|
style={{ display: "block", width: "100%", padding: "14px 18px" }}
|
|
>
|
|
<Group justify="space-between" wrap="nowrap" gap="sm">
|
|
<Stack gap={2} style={{ minWidth: 0 }}>
|
|
<Group gap={8} wrap="nowrap">
|
|
<Text
|
|
size="xs"
|
|
fw={700}
|
|
c={isComplete || isActive ? "green.7" : "dimmed"}
|
|
style={{ letterSpacing: 0.6 }}
|
|
>
|
|
STEP {index + 1}
|
|
</Text>
|
|
{isComplete ? (
|
|
<Badge size="xs" variant="light" color="green" radius="sm">
|
|
Done
|
|
</Badge>
|
|
) : null}
|
|
</Group>
|
|
<Text fw={700} size="md" lh={1.2} truncate>
|
|
{title}
|
|
</Text>
|
|
{subtitle ? (
|
|
<Text size="xs" c="dimmed" truncate>
|
|
{subtitle}
|
|
</Text>
|
|
) : null}
|
|
</Stack>
|
|
|
|
<Group gap="sm" wrap="nowrap" style={{ flexShrink: 0 }}>
|
|
{rightSlot}
|
|
<Box
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
width: 28,
|
|
height: 28,
|
|
borderRadius: 8,
|
|
background: open
|
|
? "var(--mantine-color-green-0)"
|
|
: "var(--mantine-color-gray-1)",
|
|
color: open
|
|
? "var(--mantine-color-green-7)"
|
|
: "var(--mantine-color-gray-6)",
|
|
}}
|
|
>
|
|
{locked ? (
|
|
<Lock size={14} />
|
|
) : (
|
|
<ChevronDown
|
|
size={16}
|
|
style={{
|
|
transform: open ? "rotate(180deg)" : "none",
|
|
transition: "transform 150ms ease",
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</Group>
|
|
</Group>
|
|
</UnstyledButton>
|
|
|
|
<Collapse expanded={open}>
|
|
<Box
|
|
px="lg"
|
|
pb="lg"
|
|
pt={4}
|
|
style={{ borderTop: "1px solid var(--mantine-color-gray-1)" }}
|
|
>
|
|
<Box pt="md">{children}</Box>
|
|
</Box>
|
|
</Collapse>
|
|
</Box>
|
|
</Group>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Wraps a list of <WorkflowStep> and paints the continuous vertical rail that
|
|
* connects the status circles.
|
|
*/
|
|
export function WorkflowRail({ children }: { children: ReactNode }) {
|
|
return (
|
|
<Box style={{ position: "relative" }}>
|
|
{/* connector rail behind the circles (circle is 40px → center at 20) */}
|
|
<Box
|
|
style={{
|
|
position: "absolute",
|
|
left: 19,
|
|
top: 24,
|
|
bottom: 24,
|
|
width: 2,
|
|
background:
|
|
"linear-gradient(180deg, var(--mantine-color-green-3) 0%, var(--mantine-color-gray-3) 100%)",
|
|
borderRadius: 2,
|
|
pointerEvents: "none",
|
|
}}
|
|
/>
|
|
<Stack gap="md">{children}</Stack>
|
|
</Box>
|
|
);
|
|
}
|