mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
feature: Add shadcn Tooltips to AppHeader for better UX
- Migrated serial number tooltip from native title to shadcn Tooltip - Shows formatted machine details (serial, MAC, total stitches, service count) - Better multi-line formatting with proper spacing - Added tooltip to machine status badge - Shows state description explaining current status - Added tooltip to auto-refresh spinner - Shows "Auto-refreshing machine status" - Removed redundant title attributes - Disconnect button already has clear label - Error button has Popover for details Benefits: - Consistent tooltip styling across the app - Better accessibility with ARIA attributes - Improved readability with proper formatting - Better positioning and animations 🤖 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
e1a64e9459
commit
8b6eb593d9
1 changed files with 167 additions and 135 deletions
|
|
@ -15,6 +15,12 @@ import {
|
||||||
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 { Popover, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function AppHeader() {
|
export function AppHeader() {
|
||||||
|
|
@ -61,145 +67,171 @@ export function AppHeader() {
|
||||||
const StatusIcon = stateIcons[stateVisual.iconName];
|
const StatusIcon = stateIcons[stateVisual.iconName];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
<TooltipProvider>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-4 lg:gap-8 items-center">
|
<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">
|
||||||
{/* Machine Connection Status - Responsive width column */}
|
<div className="grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-4 lg:gap-8 items-center">
|
||||||
<div className="flex items-center gap-3 w-full lg:w-[280px]">
|
{/* Machine Connection Status - Responsive width column */}
|
||||||
<div
|
<div className="flex items-center gap-3 w-full lg:w-[280px]">
|
||||||
className="w-2.5 h-2.5 bg-success-400 rounded-full animate-pulse shadow-lg shadow-success-400/50"
|
<div
|
||||||
style={{ visibility: isConnected ? "visible" : "hidden" }}
|
className="w-2.5 h-2.5 bg-success-400 rounded-full animate-pulse shadow-lg shadow-success-400/50"
|
||||||
></div>
|
style={{ visibility: isConnected ? "visible" : "hidden" }}
|
||||||
<div
|
></div>
|
||||||
className="w-2.5 h-2.5 bg-gray-400 rounded-full -ml-2.5"
|
<div
|
||||||
style={{ visibility: !isConnected ? "visible" : "hidden" }}
|
className="w-2.5 h-2.5 bg-gray-400 rounded-full -ml-2.5"
|
||||||
></div>
|
style={{ visibility: !isConnected ? "visible" : "hidden" }}
|
||||||
<div className="flex-1 min-w-0">
|
></div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex-1 min-w-0">
|
||||||
<h1 className="text-lg lg:text-xl font-bold text-white leading-tight">
|
<div className="flex items-center gap-2">
|
||||||
Respira
|
<h1 className="text-lg lg:text-xl font-bold text-white leading-tight">
|
||||||
</h1>
|
Respira
|
||||||
{isConnected && machineInfo?.serialNumber && (
|
</h1>
|
||||||
<span
|
{isConnected && machineInfo?.serialNumber && (
|
||||||
className="text-xs text-primary-200 cursor-help"
|
<Tooltip>
|
||||||
title={`Serial: ${machineInfo.serialNumber}${
|
<TooltipTrigger asChild>
|
||||||
machineInfo.macAddress
|
<span className="text-xs text-primary-200 cursor-help">
|
||||||
? `\nMAC: ${machineInfo.macAddress}`
|
• {machineInfo.serialNumber}
|
||||||
: ""
|
</span>
|
||||||
}${
|
</TooltipTrigger>
|
||||||
machineInfo.totalCount !== undefined
|
<TooltipContent className="max-w-xs">
|
||||||
? `\nTotal stitches: ${machineInfo.totalCount.toLocaleString()}`
|
<div className="text-sm space-y-1">
|
||||||
: ""
|
<p className="font-semibold">
|
||||||
}${
|
Serial: {machineInfo.serialNumber}
|
||||||
machineInfo.serviceCount !== undefined
|
</p>
|
||||||
? `\nStitches since service: ${machineInfo.serviceCount.toLocaleString()}`
|
{machineInfo.macAddress && (
|
||||||
: ""
|
<p className="text-xs">
|
||||||
}`}
|
MAC: {machineInfo.macAddress}
|
||||||
>
|
</p>
|
||||||
• {machineInfo.serialNumber}
|
)}
|
||||||
</span>
|
{machineInfo.totalCount !== undefined && (
|
||||||
)}
|
<p className="text-xs">
|
||||||
{isPolling && (
|
Total stitches:{" "}
|
||||||
<ArrowPathIcon
|
{machineInfo.totalCount.toLocaleString()}
|
||||||
className="w-3.5 h-3.5 text-primary-200 animate-spin"
|
</p>
|
||||||
title="Auto-refreshing status"
|
)}
|
||||||
/>
|
{machineInfo.serviceCount !== undefined && (
|
||||||
)}
|
<p className="text-xs">
|
||||||
</div>
|
Stitches since service:{" "}
|
||||||
<div className="flex items-center gap-2 mt-1 min-h-[32px]">
|
{machineInfo.serviceCount.toLocaleString()}
|
||||||
{isConnected ? (
|
</p>
|
||||||
<>
|
)}
|
||||||
<Button
|
</div>
|
||||||
onClick={disconnect}
|
</TooltipContent>
|
||||||
size="sm"
|
</Tooltip>
|
||||||
variant="outline"
|
|
||||||
className="gap-1.5 bg-white/10 hover:bg-danger-600 text-primary-100 hover:text-white border-white/20 hover:border-danger-600 flex-shrink-0"
|
|
||||||
title="Disconnect from machine"
|
|
||||||
aria-label="Disconnect from machine"
|
|
||||||
>
|
|
||||||
<XMarkIcon className="w-3 h-3" />
|
|
||||||
Disconnect
|
|
||||||
</Button>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="gap-1.5 px-2.5 py-1.5 sm:py-1 text-sm font-semibold bg-white/20 text-white border-white/30 flex-shrink-0"
|
|
||||||
>
|
|
||||||
<StatusIcon className="w-3 h-3" />
|
|
||||||
{machineStatusName}
|
|
||||||
</Badge>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="text-xs text-primary-200">Not Connected</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error indicator - always render to prevent layout shift */}
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
className={cn(
|
|
||||||
"gap-1.5 flex-shrink-0",
|
|
||||||
machineErrorMessage || pyodideError
|
|
||||||
? "animate-pulse hover:animate-none"
|
|
||||||
: "invisible pointer-events-none",
|
|
||||||
)}
|
|
||||||
title="Click to view error details"
|
|
||||||
aria-label="View error details"
|
|
||||||
disabled={!(machineErrorMessage || pyodideError)}
|
|
||||||
>
|
|
||||||
<ExclamationTriangleIcon className="w-3.5 h-3.5 flex-shrink-0" />
|
|
||||||
<span>
|
|
||||||
{(() => {
|
|
||||||
if (pyodideError) return "Python Error";
|
|
||||||
if (isPairingError) return "Pairing Required";
|
|
||||||
|
|
||||||
const errorMsg = machineErrorMessage || "";
|
|
||||||
|
|
||||||
// Categorize by error message content
|
|
||||||
if (
|
|
||||||
errorMsg.toLowerCase().includes("bluetooth") ||
|
|
||||||
errorMsg.toLowerCase().includes("connection")
|
|
||||||
) {
|
|
||||||
return "Connection Error";
|
|
||||||
}
|
|
||||||
if (errorMsg.toLowerCase().includes("upload")) {
|
|
||||||
return "Upload Error";
|
|
||||||
}
|
|
||||||
if (errorMsg.toLowerCase().includes("pattern")) {
|
|
||||||
return "Pattern Error";
|
|
||||||
}
|
|
||||||
if (machineError !== undefined) {
|
|
||||||
return `Machine Error`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default fallback
|
|
||||||
return "Error";
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
|
|
||||||
{/* Error popover content */}
|
|
||||||
{(machineErrorMessage || pyodideError) && (
|
|
||||||
<ErrorPopoverContent
|
|
||||||
machineError={
|
|
||||||
machineError != 0xdd ? machineError : undefined
|
|
||||||
}
|
|
||||||
isPairingError={isPairingError}
|
|
||||||
errorMessage={machineErrorMessage}
|
|
||||||
pyodideError={pyodideError}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Popover>
|
{isPolling && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div>
|
||||||
|
<ArrowPathIcon className="w-3.5 h-3.5 text-primary-200 animate-spin" />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="text-xs">Auto-refreshing machine status</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-1 min-h-[32px]">
|
||||||
|
{isConnected ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={disconnect}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="gap-1.5 bg-white/10 hover:bg-danger-600 text-primary-100 hover:text-white border-white/20 hover:border-danger-600 flex-shrink-0"
|
||||||
|
aria-label="Disconnect from machine"
|
||||||
|
>
|
||||||
|
<XMarkIcon className="w-3 h-3" />
|
||||||
|
Disconnect
|
||||||
|
</Button>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="gap-1.5 px-2.5 py-1.5 sm:py-1 text-sm font-semibold bg-white/20 text-white border-white/30 flex-shrink-0 cursor-help"
|
||||||
|
>
|
||||||
|
<StatusIcon className="w-3 h-3" />
|
||||||
|
{machineStatusName}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="text-xs">{stateVisual.description}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-primary-200">Not Connected</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error indicator - always render to prevent layout shift */}
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
className={cn(
|
||||||
|
"gap-1.5 flex-shrink-0",
|
||||||
|
machineErrorMessage || pyodideError
|
||||||
|
? "animate-pulse hover:animate-none"
|
||||||
|
: "invisible pointer-events-none",
|
||||||
|
)}
|
||||||
|
aria-label="View error details"
|
||||||
|
disabled={!(machineErrorMessage || pyodideError)}
|
||||||
|
>
|
||||||
|
<ExclamationTriangleIcon className="w-3.5 h-3.5 flex-shrink-0" />
|
||||||
|
<span>
|
||||||
|
{(() => {
|
||||||
|
if (pyodideError) return "Python Error";
|
||||||
|
if (isPairingError) return "Pairing Required";
|
||||||
|
|
||||||
|
const errorMsg = machineErrorMessage || "";
|
||||||
|
|
||||||
|
// Categorize by error message content
|
||||||
|
if (
|
||||||
|
errorMsg.toLowerCase().includes("bluetooth") ||
|
||||||
|
errorMsg.toLowerCase().includes("connection")
|
||||||
|
) {
|
||||||
|
return "Connection Error";
|
||||||
|
}
|
||||||
|
if (errorMsg.toLowerCase().includes("upload")) {
|
||||||
|
return "Upload Error";
|
||||||
|
}
|
||||||
|
if (errorMsg.toLowerCase().includes("pattern")) {
|
||||||
|
return "Pattern Error";
|
||||||
|
}
|
||||||
|
if (machineError !== undefined) {
|
||||||
|
return `Machine Error`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fallback
|
||||||
|
return "Error";
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
{/* Error popover content */}
|
||||||
|
{(machineErrorMessage || pyodideError) && (
|
||||||
|
<ErrorPopoverContent
|
||||||
|
machineError={
|
||||||
|
machineError != 0xdd ? machineError : undefined
|
||||||
|
}
|
||||||
|
isPairingError={isPairingError}
|
||||||
|
errorMessage={machineErrorMessage}
|
||||||
|
pyodideError={pyodideError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Workflow Stepper - Flexible width column */}
|
{/* Workflow Stepper - Flexible width column */}
|
||||||
<div>
|
<div>
|
||||||
<WorkflowStepper />
|
<WorkflowStepper />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
</header>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue