mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 02:13:41 +00:00
Merge pull request #23 from jhbruhn/feature/shadcn
fix: ui updates and canvas overflow
This commit is contained in:
commit
91d248db6f
8 changed files with 239 additions and 50 deletions
53
package-lock.json
generated
53
package-lock.json
generated
|
|
@ -14,6 +14,7 @@
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
|
@ -3614,6 +3615,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/number": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/primitive": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
|
|
@ -3773,6 +3780,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-direction": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||||
|
|
@ -4096,6 +4118,37 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-scroll-area": {
|
||||||
|
"version": "1.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
|
||||||
|
"integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/number": "1.1.1",
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-separator": {
|
"node_modules/@radix-ui/react-separator": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -283,9 +283,9 @@ export function PatternCanvas() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-4 pt-0 pb-4 flex-1 flex flex-col">
|
<CardContent className="px-4 pt-0 pb-4 flex-1 flex flex-col min-h-0">
|
||||||
<div
|
<div
|
||||||
className="relative w-full h-[400px] sm:h-[500px] lg:flex-1 lg:min-h-0 border border-gray-300 dark:border-gray-600 rounded bg-gray-200 dark:bg-gray-900 overflow-hidden"
|
className="relative w-full flex-1 min-h-0 border border-gray-300 dark:border-gray-600 rounded bg-gray-200 dark:bg-gray-900 overflow-hidden"
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
{containerSize.width > 0 && (
|
{containerSize.width > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useRef, useEffect, useState, useMemo } from "react";
|
import { useRef, useEffect, useMemo } from "react";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useMachineStore } from "../stores/useMachineStore";
|
import { useMachineStore } from "../stores/useMachineStore";
|
||||||
import { usePatternStore } from "../stores/usePatternStore";
|
import { usePatternStore } from "../stores/usePatternStore";
|
||||||
|
|
@ -26,6 +26,7 @@ import {
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
export function ProgressMonitor() {
|
export function ProgressMonitor() {
|
||||||
// Machine store
|
// Machine store
|
||||||
|
|
@ -52,8 +53,6 @@ export function ProgressMonitor() {
|
||||||
// Pattern store
|
// Pattern store
|
||||||
const pesData = usePatternStore((state) => state.pesData);
|
const pesData = usePatternStore((state) => state.pesData);
|
||||||
const currentBlockRef = useRef<HTMLDivElement>(null);
|
const currentBlockRef = useRef<HTMLDivElement>(null);
|
||||||
const colorBlocksScrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [showGradient, setShowGradient] = useState(true);
|
|
||||||
|
|
||||||
// State indicators
|
// State indicators
|
||||||
const isMaskTraceComplete =
|
const isMaskTraceComplete =
|
||||||
|
|
@ -135,31 +134,6 @@ export function ProgressMonitor() {
|
||||||
}
|
}
|
||||||
}, [currentBlockIndex]);
|
}, [currentBlockIndex]);
|
||||||
|
|
||||||
// Handle scroll to detect if at bottom
|
|
||||||
const handleColorBlocksScroll = () => {
|
|
||||||
if (colorBlocksScrollRef.current) {
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } =
|
|
||||||
colorBlocksScrollRef.current;
|
|
||||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 5; // 5px threshold
|
|
||||||
setShowGradient(!isAtBottom);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check initial scroll state and update on resize
|
|
||||||
useEffect(() => {
|
|
||||||
const checkScrollable = () => {
|
|
||||||
if (colorBlocksScrollRef.current) {
|
|
||||||
const { scrollHeight, clientHeight } = colorBlocksScrollRef.current;
|
|
||||||
const isScrollable = scrollHeight > clientHeight;
|
|
||||||
setShowGradient(isScrollable);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkScrollable();
|
|
||||||
window.addEventListener("resize", checkScrollable);
|
|
||||||
return () => window.removeEventListener("resize", checkScrollable);
|
|
||||||
}, [colorBlocks]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-0 gap-0 lg:h-full border-l-4 border-accent-600 dark:border-accent-500 flex flex-col lg:overflow-hidden">
|
<Card className="p-0 gap-0 lg:h-full border-l-4 border-accent-600 dark:border-accent-500 flex flex-col lg:overflow-hidden">
|
||||||
<CardHeader className="p-4 pb-3">
|
<CardHeader className="p-4 pb-3">
|
||||||
|
|
@ -242,12 +216,8 @@ export function ProgressMonitor() {
|
||||||
<h4 className="text-xs font-semibold mb-2 text-gray-700 dark:text-gray-300 flex-shrink-0">
|
<h4 className="text-xs font-semibold mb-2 text-gray-700 dark:text-gray-300 flex-shrink-0">
|
||||||
Color Blocks
|
Color Blocks
|
||||||
</h4>
|
</h4>
|
||||||
<div className="relative lg:flex-1 lg:min-h-0">
|
<ScrollArea className="lg:flex-1 lg:h-0">
|
||||||
<div
|
<div className="flex flex-col gap-2 pr-4">
|
||||||
ref={colorBlocksScrollRef}
|
|
||||||
onScroll={handleColorBlocksScroll}
|
|
||||||
className="lg:absolute lg:inset-0 flex flex-col gap-2 lg:overflow-y-auto scroll-smooth pr-1 [&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar-track]:bg-gray-100 dark:[&::-webkit-scrollbar-track]:bg-gray-700 [&::-webkit-scrollbar-thumb]:bg-primary-600 dark:[&::-webkit-scrollbar-thumb]:bg-primary-500 [&::-webkit-scrollbar-thumb]:rounded-full"
|
|
||||||
>
|
|
||||||
{colorBlocks.map((block, index) => {
|
{colorBlocks.map((block, index) => {
|
||||||
const isCompleted = currentStitch >= block.endStitch;
|
const isCompleted = currentStitch >= block.endStitch;
|
||||||
const isCurrent = index === currentBlockIndex;
|
const isCurrent = index === currentBlockIndex;
|
||||||
|
|
@ -362,11 +332,7 @@ export function ProgressMonitor() {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{/* Gradient overlay to indicate more content below - only on desktop and when not at bottom */}
|
</ScrollArea>
|
||||||
{showGradient && (
|
|
||||||
<div className="hidden lg:block absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-white dark:from-gray-800 to-transparent pointer-events-none" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
56
src/components/ui/scroll-area.tsx
Normal file
56
src/components/ui/scroll-area.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function ScrollArea({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
data-slot="scroll-area"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollBar({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none p-px transition-colors select-none",
|
||||||
|
orientation === "vertical" &&
|
||||||
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
|
orientation === "horizontal" &&
|
||||||
|
"h-2.5 flex-col border-t border-t-transparent",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
className="bg-border relative flex-1 rounded-full"
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar };
|
||||||
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