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