SignalDot.tsx2.1 KBView on GitHub import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import type { FieldSignal } from '@/modules/crm/types';
const SIGNAL_LABELS: Record<FieldSignal, string> = {
red: 'Red',
yellow: 'Yellow',
green: 'Green',
};
const SIGNAL_BG: Record<FieldSignal, string> = {
red: 'bg-red-500',
yellow: 'bg-amber-500',
green: 'bg-emerald-500',
};
/**
* A small filled circle indicating a field's red/yellow/green signal, with an
* optional popover showing the signal reasoning. When `signal` is null, renders
* a muted hollow dot.
*/
export function SignalDot({
signal,
reasoning,
size = 12,
}: {
signal: FieldSignal | null;
reasoning?: string | null;
size?: number;
}) {
const dot =
signal == null ? (
<span
className="block rounded-full border border-muted-foreground/40"
style={{ width: size, height: size }}
/>
) : (
<span
className={cn('block rounded-full', SIGNAL_BG[signal])}
style={{ width: size, height: size }}
/>
);
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
data-signal={signal ?? 'none'}
aria-label={`Signal: ${signal == null ? 'None' : SIGNAL_LABELS[signal]}`}
className="flex shrink-0 items-center justify-center rounded-full p-0.5 transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
onClick={(e) => e.stopPropagation()}
>
{dot}
</button>
</PopoverTrigger>
<PopoverContent side="left" align="start" className="w-64 space-y-2 p-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Signal</span>
<span className="text-sm text-muted-foreground">
{signal == null ? 'None' : SIGNAL_LABELS[signal]}
</span>
</div>
{reasoning && <p className="text-sm leading-snug text-muted-foreground">{reasoning}</p>}
</PopoverContent>
</Popover>
);
}