mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 18:33:41 +00:00
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:
parent
4992c33bf1
commit
a077dea68e
3 changed files with 122 additions and 9 deletions
|
|
@ -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">
|
||||||
|
|
|
||||||
84
src/components/ui/status-indicator.tsx
Normal file
84
src/components/ui/status-indicator.tsx
Normal 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;
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue