diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index 2f1fe20..516b6ed 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -3,7 +3,10 @@ import { useMachineStore } from "../stores/useMachineStore"; import { useUIStore } from "../stores/useUIStore"; import { WorkflowStepper } from "./WorkflowStepper"; import { ErrorPopoverContent } from "./ErrorPopover"; -import { getStateVisualInfo } from "../utils/machineStateHelpers"; +import { + getStateVisualInfo, + getStatusIndicatorState, +} from "../utils/machineStateHelpers"; import { CheckCircleIcon, BoltIcon, @@ -14,6 +17,7 @@ import { } from "@heroicons/react/24/solid"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import StatusIndicator from "@/components/ui/status-indicator"; import { Popover, PopoverTrigger } from "@/components/ui/popover"; import { Tooltip, @@ -66,20 +70,18 @@ export function AppHeader() { }; const StatusIcon = stateIcons[stateVisual.iconName]; + // Get connection indicator state (idle when disconnected, state-dependent when connected) + const connectionIndicatorState = isConnected + ? getStatusIndicatorState(machineStatus) + : "idle"; + return (
{/* Machine Connection Status - Responsive width column */}
-
-
+

diff --git a/src/components/ui/status-indicator.tsx b/src/components/ui/status-indicator.tsx new file mode 100644 index 0000000..8e318e0 --- /dev/null +++ b/src/components/ui/status-indicator.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { cn } from "@/lib/utils"; + +interface StatusIndicatorProps { + state: "active" | "down" | "fixing" | "idle"; + label?: string; + className?: string; + size?: "sm" | "md" | "lg"; + labelClassName?: string; +} + +const getStateColors = (state: StatusIndicatorProps["state"]) => { + switch (state) { + case "active": + return { dot: "bg-green-500", ping: "bg-green-300" }; + case "down": + return { dot: "bg-red-500", ping: "bg-red-300" }; + case "fixing": + return { dot: "bg-yellow-500", ping: "bg-yellow-300" }; + case "idle": + default: + return { dot: "bg-slate-700", ping: "bg-slate-400" }; + } +}; + +const getSizeClasses = (size: StatusIndicatorProps["size"]) => { + switch (size) { + case "sm": + return { dot: "h-2 w-2", ping: "h-2 w-2" }; + case "lg": + return { dot: "h-4 w-4", ping: "h-4 w-4" }; + case "md": + default: + return { dot: "h-3 w-3", ping: "h-3 w-3" }; + } +}; + +const StatusIndicator: React.FC = ({ + state = "idle", + label, + className, + size = "md", + labelClassName, +}) => { + const shouldAnimate = + state === "active" || state === "fixing" || state === "down"; + const colors = getStateColors(state); + const sizeClasses = getSizeClasses(size); + + return ( +
+
+ {shouldAnimate && ( + + )} + +
+ {label && ( +

+ {label} +

+ )} +
+ ); +}; + +export default StatusIndicator; diff --git a/src/utils/machineStateHelpers.ts b/src/utils/machineStateHelpers.ts index 8548bc6..231c84c 100644 --- a/src/utils/machineStateHelpers.ts +++ b/src/utils/machineStateHelpers.ts @@ -205,3 +205,30 @@ export function getStateVisualInfo(status: MachineStatus): StateVisualInfo { return visualMap[category]; } + +/** + * Map machine state category to status indicator state. + * Returns the appropriate state for the StatusIndicator component. + */ +export function getStatusIndicatorState( + status: MachineStatus, +): "active" | "down" | "fixing" | "idle" { + const category = getMachineStateCategory(status); + + switch (category) { + case MachineStateCategory.IDLE: + return "active"; // Gray, no animation + case MachineStateCategory.ACTIVE: + return "active"; // Green with animation + case MachineStateCategory.WAITING: + return "fixing"; // Yellow with animation + case MachineStateCategory.COMPLETE: + return "active"; // Green with animation + case MachineStateCategory.INTERRUPTED: + return "down"; // Red with animation + case MachineStateCategory.ERROR: + return "down"; // Red with animation + default: + return "idle"; + } +}