feature: Replace connection indicator with shadcn StatusIndicator

Replace the custom connection indicator dots with the shadcn StatusIndicator
component for better visual consistency and state indication.

Changes:
- Install status-indicator component from 8starlabs shadcn registry
- Add getStatusIndicatorState() helper to map MachineStateCategory to StatusIndicator states
- Replace connection indicator divs with StatusIndicator component in AppHeader
- Connection indicator now shows state-dependent colors:
  - Green (active): Connected and ready, sewing, or complete
  - Yellow (fixing): Connected and waiting for user action
  - Red (down): Connected but interrupted or in error state
  - Gray (idle): Disconnected
- Remove unused color prop from StatusIndicator component

The StatusIndicator provides animated visual feedback for different machine
states, making it easier for users to understand the current system status at
a glance.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik Bruhn 2025-12-21 13:29:27 +01:00
parent 4992c33bf1
commit a077dea68e
3 changed files with 122 additions and 9 deletions

View file

@ -3,7 +3,10 @@ import { useMachineStore } from "../stores/useMachineStore";
import { useUIStore } from "../stores/useUIStore"; import { useUIStore } from "../stores/useUIStore";
import { WorkflowStepper } from "./WorkflowStepper"; import { WorkflowStepper } from "./WorkflowStepper";
import { ErrorPopoverContent } from "./ErrorPopover"; import { ErrorPopoverContent } from "./ErrorPopover";
import { getStateVisualInfo } from "../utils/machineStateHelpers"; import {
getStateVisualInfo,
getStatusIndicatorState,
} from "../utils/machineStateHelpers";
import { import {
CheckCircleIcon, CheckCircleIcon,
BoltIcon, BoltIcon,
@ -14,6 +17,7 @@ import {
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import StatusIndicator from "@/components/ui/status-indicator";
import { Popover, PopoverTrigger } from "@/components/ui/popover"; import { Popover, PopoverTrigger } from "@/components/ui/popover";
import { import {
Tooltip, Tooltip,
@ -66,20 +70,18 @@ export function AppHeader() {
}; };
const StatusIcon = stateIcons[stateVisual.iconName]; const StatusIcon = stateIcons[stateVisual.iconName];
// Get connection indicator state (idle when disconnected, state-dependent when connected)
const connectionIndicatorState = isConnected
? getStatusIndicatorState(machineStatus)
: "idle";
return ( return (
<TooltipProvider> <TooltipProvider>
<header className="bg-gradient-to-r from-primary-600 via-primary-700 to-primary-800 dark:from-primary-700 dark:via-primary-800 dark:to-primary-900 px-4 sm:px-6 lg:px-8 py-3 shadow-lg border-b-2 border-primary-900/20 dark:border-primary-800/30 flex-shrink-0"> <header className="bg-gradient-to-r from-primary-600 via-primary-700 to-primary-800 dark:from-primary-700 dark:via-primary-800 dark:to-primary-900 px-4 sm:px-6 lg:px-8 py-3 shadow-lg border-b-2 border-primary-900/20 dark:border-primary-800/30 flex-shrink-0">
<div className="grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-4 lg:gap-8 items-center"> <div className="grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-4 lg:gap-8 items-center">
{/* Machine Connection Status - Responsive width column */} {/* Machine Connection Status - Responsive width column */}
<div className="flex items-center gap-3 w-full lg:w-[280px]"> <div className="flex items-center gap-3 w-full lg:w-[280px]">
<div <StatusIndicator state={connectionIndicatorState} size="sm" />
className="w-2.5 h-2.5 bg-success-400 rounded-full animate-pulse shadow-lg shadow-success-400/50"
style={{ visibility: isConnected ? "visible" : "hidden" }}
></div>
<div
className="w-2.5 h-2.5 bg-gray-400 rounded-full -ml-2.5"
style={{ visibility: !isConnected ? "visible" : "hidden" }}
></div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h1 className="text-lg lg:text-xl font-bold text-white leading-tight"> <h1 className="text-lg lg:text-xl font-bold text-white leading-tight">

View file

@ -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<StatusIndicatorProps> = ({
state = "idle",
label,
className,
size = "md",
labelClassName,
}) => {
const shouldAnimate =
state === "active" || state === "fixing" || state === "down";
const colors = getStateColors(state);
const sizeClasses = getSizeClasses(size);
return (
<div className={cn("flex items-center gap-2", className)}>
<div className="relative flex items-center">
{shouldAnimate && (
<span
className={cn(
"absolute inline-flex rounded-full opacity-75 animate-ping",
sizeClasses.ping,
colors.ping,
)}
/>
)}
<span
className={cn(
"relative inline-flex rounded-full",
sizeClasses.dot,
colors.dot,
)}
/>
</div>
{label && (
<p
className={cn(
"text-sm text-slate-700 dark:text-slate-300",
labelClassName,
)}
>
{label}
</p>
)}
</div>
);
};
export default StatusIndicator;

View file

@ -205,3 +205,30 @@ export function getStateVisualInfo(status: MachineStatus): StateVisualInfo {
return visualMap[category]; 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";
}
}