feat(status-badge): CSS hover tooltip — replaces native title attribute

title= attribute tooltip is browser-native, tiny, and often invisible.
New StatusBadge component uses a Tailwind group-hover absolute panel
that appears immediately on hover with:
  - Clear heading per status
  - 'Meaning' and 'Action' lines
  - Arrow pointing to the badge
  - 200ms fade-in transition

Used in TechniquesPage (list table) and TechniqueDetailPage (header).
This commit is contained in:
kitos
2026-06-02 10:42:13 +02:00
parent 546b5692f0
commit e82af44a6c
3 changed files with 126 additions and 34 deletions
+120
View File
@@ -0,0 +1,120 @@
/**
* StatusBadge — renders a technique coverage status pill with a
* CSS-only hover tooltip explaining what the status means.
*/
import type { TechniqueStatus } from "../types/models";
const BADGE_COLORS: Record<TechniqueStatus, string> = {
validated: "bg-green-900/50 text-green-400 border-green-500/30",
partial: "bg-yellow-900/50 text-yellow-400 border-yellow-500/30",
in_progress: "bg-blue-900/50 text-blue-400 border-blue-500/30",
not_covered: "bg-red-900/50 text-red-400 border-red-500/30",
not_evaluated: "bg-gray-800/50 text-gray-400 border-gray-600/30",
review_required:"bg-orange-900/50 text-orange-400 border-orange-500/30",
};
const BADGE_LABELS: Record<TechniqueStatus, string> = {
validated: "Validated",
partial: "Partial",
in_progress: "In Progress",
not_covered: "Not Covered",
not_evaluated: "Not Evaluated",
review_required:"Review Required",
};
interface TooltipLine { label: string; text: string }
const TOOLTIPS: Record<TechniqueStatus, { heading: string; lines: TooltipLine[] }> = {
validated: {
heading: "✅ Validated",
lines: [
{ label: "Meaning", text: "≥2 tests executed. Blue Team detected the attack in all of them." },
{ label: "Action", text: "Maintain and re-test periodically." },
],
},
partial: {
heading: "🟡 Partial",
lines: [
{ label: "Meaning", text: "Only 1 validated test, some tests still pending, or detection was not 100%." },
{ label: "Action", text: "Run more tests and ensure all are validated with 'detected'." },
],
},
in_progress: {
heading: "🔵 In Progress",
lines: [
{ label: "Meaning", text: "Tests exist but none have been validated yet (draft, executing, or in review)." },
{ label: "Action", text: "Complete the Red/Blue validation workflow." },
],
},
not_covered: {
heading: "🔴 Not Covered",
lines: [
{ label: "Meaning", text: "Tests were run but Blue Team did NOT detect the attack — coverage gap." },
{ label: "Action", text: "Improve detection rules and re-test." },
],
},
not_evaluated: {
heading: "⚫ Not Evaluated",
lines: [
{ label: "Meaning", text: "No tests have been created for this technique yet." },
{ label: "Action", text: "Create a test from the available templates." },
],
},
review_required: {
heading: "🟠 Review Required",
lines: [
{ label: "Meaning", text: "MITRE updated this technique, or new intel / detection rules were added." },
{ label: "Action", text: "A lead should review the changes and mark as reviewed." },
],
},
};
interface StatusBadgeProps {
status: TechniqueStatus;
/** Extra classes on the outer wrapper (e.g. sizing) */
className?: string;
/** Size variant — defaults to 'md' */
size?: "sm" | "md";
}
export default function StatusBadge({ status, className = "", size = "md" }: StatusBadgeProps) {
const tip = TOOLTIPS[status];
const pill = size === "sm"
? "px-2 py-0.5 text-[10px]"
: "px-2.5 py-0.5 text-xs";
return (
<span className={`relative group inline-flex ${className}`}>
{/* The badge itself */}
<span
className={`inline-flex cursor-help items-center rounded-full border font-medium ${pill} ${BADGE_COLORS[status]}`}
>
{BADGE_LABELS[status]}
</span>
{/* Tooltip — appears above the badge on hover */}
<span
className="
pointer-events-none absolute bottom-full left-1/2 z-50 mb-2
w-64 -translate-x-1/2
rounded-xl border border-gray-700 bg-gray-900 p-3 shadow-xl
opacity-0 transition-opacity duration-150
group-hover:opacity-100
"
>
<p className="mb-2 text-xs font-semibold text-white">{tip.heading}</p>
{tip.lines.map(({ label, text }) => (
<div key={label} className="mb-1 last:mb-0">
<span className="text-[10px] font-medium uppercase tracking-wide text-gray-500">
{label}
</span>
<p className="text-[11px] leading-snug text-gray-300">{text}</p>
</div>
))}
{/* Arrow pointing down */}
<span className="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-700" />
</span>
</span>
);
}