import { useMemo } from "react"; import type { ResidualNode } from "../types"; import ResidualNodeView from "../components/ResidualNode"; import "./ViolationsPanel.css"; export interface ViolationsPanelProps { propertyNames: string[]; violations: string[]; residuals?: Record; onJumpToFirstViolation: () => void; hasFirstViolation: boolean; /** When true, only render violated rows and hide the header button row. */ violationsOnly?: boolean; } type Status = "violated" | "pending" | "holds"; const STATUS_ORDER: Record = { violated: 0, pending: 1, holds: 2, }; function statusFor( name: string, violations: Set, residuals?: Record, ): Status { if (violations.has(name)) { return "violated"; } const residual = residuals?.[name]; if (residual && residual.op === "true") { return "holds"; } return "pending"; } export default function ViolationsPanel({ propertyNames, violations, residuals, onJumpToFirstViolation, hasFirstViolation, violationsOnly = false, }: ViolationsPanelProps) { const violationSet = useMemo(() => new Set(violations), [violations]); const rows = useMemo(() => { const sorted = [...propertyNames].sort((a, b) => a.localeCompare(b)); const all = sorted.map((name) => ({ name, status: statusFor(name, violationSet, residuals), })); const filtered = violationsOnly ? all.filter((row) => row.status === "violated") : all; return filtered.sort((a, b) => { const groupDelta = STATUS_ORDER[a.status] - STATUS_ORDER[b.status]; if (groupDelta !== 0) { return groupDelta; } return a.name.localeCompare(b.name); }); }, [propertyNames, violationSet, residuals, violationsOnly]); if (violationsOnly && rows.length === 0) { return
no violations
; } return (
{violationsOnly ? null : (

properties

)}
    {rows.map(({ name, status }) => { const residual = residuals?.[name]; return (
  • {status} {name}
    {residual ? (
    residual
    ) : null}
  • ); })}
); }