- Boost text contrast in both themes - Strengthen border visibility (subtle borders now visible) - Convert 39 files from hardcoded dark:/light: to CSS vars - Tertiary text now more readable on both backgrounds
153 lines
6 KiB
TypeScript
153 lines
6 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
||
import { Bed } from './GrowRoomHeatmap';
|
||
|
||
/**
|
||
* BedTooltip - Hover tooltip showing bed details
|
||
*
|
||
* Displays key information about a bed when hovering over it.
|
||
* Positioned near the mouse cursor.
|
||
*/
|
||
|
||
interface BedTooltipProps {
|
||
bed: Bed;
|
||
position: { x: number; y: number };
|
||
onClose: () => void;
|
||
}
|
||
|
||
export default function BedTooltip({ bed, position, onClose }: BedTooltipProps) {
|
||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||
|
||
// Close on click outside
|
||
useEffect(() => {
|
||
function handleClickOutside(event: MouseEvent) {
|
||
if (tooltipRef.current && !tooltipRef.current.contains(event.target as Node)) {
|
||
onClose();
|
||
}
|
||
}
|
||
|
||
document.addEventListener('mousedown', handleClickOutside);
|
||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||
}, [onClose]);
|
||
|
||
const getHealthLabel = (score: number): string => {
|
||
if (score >= 90) return 'Excellent';
|
||
if (score >= 70) return 'Good';
|
||
if (score >= 50) return 'Fair';
|
||
if (score >= 30) return 'Needs Attention';
|
||
return 'Critical';
|
||
};
|
||
|
||
const getHealthColor = (score: number): string => {
|
||
if (score >= 90) return 'text-[var(--color-primary)] dark:text-emerald-400';
|
||
if (score >= 70) return 'text-[var(--color-primary)] dark:text-emerald-300';
|
||
if (score >= 50) return 'text-yellow-500 dark:text-yellow-300';
|
||
if (score >= 30) return 'text-orange-500 dark:text-orange-300';
|
||
return 'text-red-600 dark:text-red-400';
|
||
};
|
||
|
||
return (
|
||
<div
|
||
ref={tooltipRef}
|
||
className="fixed z-50 bg-[var(--color-bg-elevated)] rounded-lg shadow-2xl border border-[var(--color-border-default)] p-4 min-w-[280px]"
|
||
style={{
|
||
left: position.x + 10,
|
||
top: position.y + 10,
|
||
}}
|
||
>
|
||
{/* Header */}
|
||
<div className="flex items-start justify-between mb-3">
|
||
<div>
|
||
<div className="text-sm font-medium text-slate-600 dark:text-[var(--color-text-tertiary)]">
|
||
Bed {bed.bed_id}
|
||
</div>
|
||
{bed.plant_batch_id && (
|
||
<div className="text-xs text-[var(--color-text-tertiary)] dark:text-[var(--color-text-tertiary)] mt-1">
|
||
Batch: {bed.plant_batch_id}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="text-[var(--color-text-tertiary)] hover:text-slate-600 dark:hover:text-slate-300"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
{/* Health Score */}
|
||
<div className="mb-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-slate-600 dark:text-[var(--color-text-tertiary)]">
|
||
Health Score
|
||
</span>
|
||
<span className={`text-lg font-bold ${getHealthColor(bed.health_score)}`}>
|
||
{bed.health_score}
|
||
</span>
|
||
</div>
|
||
<div className="text-xs text-[var(--color-text-tertiary)] dark:text-[var(--color-text-tertiary)] mt-1">
|
||
{getHealthLabel(bed.health_score)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Sensors */}
|
||
{bed.sensors && (
|
||
<div className="space-y-2 mb-3">
|
||
<div className="text-xs font-medium text-slate-600 dark:text-[var(--color-text-tertiary)] mb-2">
|
||
Sensor Readings
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{bed.sensors.temp !== undefined && (
|
||
<SensorReading
|
||
label="Temp"
|
||
value={`${bed.sensors.temp.toFixed(1)}°F`}
|
||
/>
|
||
)}
|
||
{bed.sensors.humidity !== undefined && (
|
||
<SensorReading
|
||
label="Humidity"
|
||
value={`${bed.sensors.humidity.toFixed(0)}%`}
|
||
/>
|
||
)}
|
||
{bed.sensors.ec !== undefined && (
|
||
<SensorReading
|
||
label="EC"
|
||
value={bed.sensors.ec.toFixed(2)}
|
||
/>
|
||
)}
|
||
{bed.sensors.par !== undefined && (
|
||
<SensorReading
|
||
label="PAR"
|
||
value={bed.sensors.par.toFixed(0)}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Last Alert */}
|
||
{bed.last_alert && (
|
||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded p-2">
|
||
<div className="text-xs font-medium text-red-700 dark:text-red-400">
|
||
⚠️ Last Alert: {bed.last_alert}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Actions */}
|
||
<div className="mt-3 pt-3 border-t border-[var(--color-border-default)]">
|
||
<button className="text-sm text-[var(--color-primary)] dark:text-emerald-400 hover:underline">
|
||
View Details →
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SensorReading({ label, value }: { label: string; value: string }) {
|
||
return (
|
||
<div className="bg-slate-50 dark:bg-slate-700/50 rounded p-2">
|
||
<div className="text-xs text-[var(--color-text-tertiary)]">{label}</div>
|
||
<div className="text-sm font-medium text-[var(--color-text-primary)]">{value}</div>
|
||
</div>
|
||
);
|
||
}
|