Files
cve-dashboard/docs/design-system-redesign/ui_kits/compliance/CompPrimitives.jsx

619 lines
28 KiB
React
Raw Normal View History

// CompPrimitives.jsx — primitives for the Compliance page kit.
// Lifted directly from frontend/src/components/pages/CompliancePage.js.
// Identity color is teal (#14B8A6); status colors map green/amber/red onto
// "Meets/Exceeds Target", "Within 15% of Target", and "Below 15% of Target".
const { useState: useCompState, useRef: useCompRef } = React;
/* Tokens
Two layers:
Status drives every percentage display + the worst-status
ribbon on metric cards. Always one of three.
Category owns the colored MetricBadge that flags which
program a failing metric belongs to. */
const C_COLORS = {
teal: '#14B8A6',
tealMid: '#5EEAD4',
green: '#10B981',
amber: '#F59E0B',
red: '#EF4444',
sky: '#0EA5E9',
purple: '#8B5CF6',
orange: '#F97316',
slate: '#64748B',
};
const STATUS_COLOR = {
'Meets/Exceeds Target': C_COLORS.green,
'Within 15% of Target': C_COLORS.amber,
'Below 15% of Target': C_COLORS.red,
};
const CATEGORY_COLORS = {
'Vulnerability Management': C_COLORS.red,
'Access & MFA': C_COLORS.amber,
'Logging & Monitoring': C_COLORS.purple,
'End-of-Life OS': C_COLORS.orange,
'Decommissioned Assets': C_COLORS.slate,
'Asset Data Quality': C_COLORS.slate,
'Application Security': C_COLORS.sky,
'Disaster Recovery': C_COLORS.teal,
'Endpoint Protection': C_COLORS.orange,
};
const statusColor = s => STATUS_COLOR[s] || C_COLORS.red;
const pctDisplay = p => `${Math.round(p * 100)}%`;
const cAlpha = (hex, a) => {
const h = hex.replace('#', '');
return `rgba(${parseInt(h.slice(0,2),16)},${parseInt(h.slice(2,4),16)},${parseInt(h.slice(4,6),16)},${a})`;
};
/* PageHeader
AEO Compliance title in teal w/ glow, last-report meta beneath,
refresh + upload-report on the right. Mirrors the KB / Reporting
header pattern but with teal instead of green. */
function CompPageHeader({ title = 'AEO Compliance', lastReport, networkScore, verticalScore, onRefresh, onUpload, onRollback, isAdmin }) {
return (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24, gap: 16 }}>
<div>
<h2 style={{
margin: '0 0 6px 0',
fontFamily: 'var(--font-mono)', fontSize: 24, fontWeight: 700,
color: C_COLORS.teal, textTransform: 'uppercase', letterSpacing: '0.1em',
textShadow: `0 0 16px ${cAlpha(C_COLORS.teal, 0.4)}`,
}}>{title}</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', fontFamily: 'var(--font-mono)', fontSize: 11 }}>
{lastReport ? (
<>
<span style={{ color: 'var(--fg-disabled)' }}>
Last report: <span style={{ color: 'var(--fg-2)' }}>{lastReport}</span>
</span>
{isAdmin && (
<button onClick={onRollback} style={{
background: 'transparent', border: '1px solid rgba(239,68,68,0.25)',
borderRadius: 4, padding: '2px 6px', cursor: 'pointer',
color: 'var(--fg-2)', display: 'inline-flex', alignItems: 'center', gap: 4,
fontSize: 10, fontFamily: 'var(--font-mono)',
}}>
<CompIcon name="rotate" size={10} color="currentColor" /> Rollback
</button>
)}
</>
) : (
<span style={{ color: 'var(--fg-disabled)' }}>No reports uploaded</span>
)}
{networkScore != null && (
<span style={{ color: 'var(--fg-2)' }}>Network: <span style={{ color: C_COLORS.teal }}>{networkScore}</span></span>
)}
{verticalScore != null && (
<span style={{ color: 'var(--fg-2)' }}>Vertical: <span style={{ color: C_COLORS.teal }}>{verticalScore}</span></span>
)}
</div>
</div>
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
<CompIconButton icon="refresh" onClick={onRefresh} />
<CompButton variant="primary" icon="upload" onClick={onUpload}>Upload Report</CompButton>
</div>
</div>
);
}
/* ── Buttons ───────────────────────────────────────────────────── */
function CompButton({ variant = 'neutral', icon, size = 'md', children, ...rest }) {
const [hover, setHover] = useCompState(false);
const v = {
primary: { bg: hover ? cAlpha(C_COLORS.teal, 0.28) : cAlpha(C_COLORS.teal, 0.18), bd: C_COLORS.teal, fg: C_COLORS.teal },
neutral: { bg: hover ? cAlpha(C_COLORS.teal, 0.10) : 'transparent', bd: cAlpha(C_COLORS.teal, 0.30), fg: C_COLORS.teal },
danger: { bg: hover ? 'rgba(239,68,68,0.18)' : 'rgba(239,68,68,0.10)', bd: C_COLORS.red, fg: C_COLORS.red },
ghost: { bg: hover ? 'rgba(255,255,255,0.04)' : 'transparent', bd: 'rgba(100,116,139,0.40)', fg: 'var(--fg-2)' },
}[variant];
const padX = size === 'sm' ? 10 : 16;
const padY = size === 'sm' ? 4 : 8;
const fs = size === 'sm' ? 11 : 12;
return (
<button
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: `${padY}px ${padX}px`, borderRadius: 6,
background: v.bg, border: `1px solid ${v.bd}`, color: v.fg,
fontFamily: 'var(--font-mono)', fontSize: fs, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
cursor: 'pointer', transition: 'all 160ms ease', whiteSpace: 'nowrap',
}}
{...rest}
>
{icon && <CompIcon name={icon} size={fs + 2} color={v.fg} />}
{children}
</button>
);
}
function CompIconButton({ icon, onClick, color = C_COLORS.teal }) {
const [hover, setHover] = useCompState(false);
return (
<button onClick={onClick}
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
background: hover ? cAlpha(color, 0.10) : 'transparent',
border: `1px solid ${hover ? color : cAlpha(color, 0.25)}`,
borderRadius: 6, padding: 8, cursor: 'pointer',
color: hover ? color : 'var(--fg-2)',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 160ms ease',
}}>
<CompIcon name={icon} size={16} color="currentColor" />
</button>
);
}
/* ── TeamTabs ──────────────────────────────────────────────────── */
function TeamTabs({ teams, active, onChange }) {
return (
<div style={{ display: 'flex', gap: 6, marginBottom: 24 }}>
{teams.map(team => {
const on = active === team;
return (
<button key={team} onClick={() => onChange(team)} style={{
padding: '8px 18px', cursor: 'pointer',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.06em',
borderRadius: 6,
border: `1px solid ${on ? C_COLORS.teal : cAlpha(C_COLORS.teal, 0.20)}`,
background: on ? cAlpha(C_COLORS.teal, 0.18) : 'transparent',
color: on ? C_COLORS.teal : 'var(--fg-disabled)',
transition: 'all 160ms ease',
}}>{team}</button>
);
})}
</div>
);
}
/* VariantPill
The compliance % pill that lives inside MetricHealthCard. One per
priority/variant within a metric family. Dot only shown when the
variant isn't already meeting target green pills stay quiet. */
function VariantPill({ status, pct, label }) {
const color = statusColor(status);
const isOk = status === 'Meets/Exceeds Target';
return (
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 4,
padding: '2px 7px',
background: cAlpha(color, 0.12),
border: `1px solid ${cAlpha(color, 0.25)}`,
borderRadius: 3,
fontFamily: 'var(--font-mono)', fontSize: 10,
color: 'var(--fg-2)', whiteSpace: 'nowrap',
}}>
{!isOk && (
<span style={{
display: 'inline-block', width: 4, height: 4, borderRadius: '50%',
background: color, boxShadow: `0 0 5px ${color}`,
}} />
)}
{label && <span style={{ color: 'var(--fg-disabled)' }}>{label}</span>}
<span style={{ color, fontWeight: 600 }}>{pctDisplay(pct)}</span>
</span>
);
}
/* StatusRibbon
The lozenge at the bottom of MetricHealthCard. "OK" when meeting,
abbreviated status text otherwise. */
function StatusRibbon({ status }) {
const color = statusColor(status);
const isOk = status === 'Meets/Exceeds Target';
return (
<div style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
fontFamily: 'var(--font-mono)', fontSize: 10,
textTransform: 'uppercase', letterSpacing: '0.04em',
color, padding: '3px 9px',
background: cAlpha(color, 0.10),
borderRadius: 999,
border: `1px solid ${cAlpha(color, 0.30)}`,
}}>
<span style={{
display: 'inline-block', width: 5, height: 5, borderRadius: '50%',
background: color, boxShadow: isOk ? 'none' : `0 0 6px ${color}`,
}} />
{isOk ? 'OK' : status.replace(' of Target', '')}
</div>
);
}
/* MetricHealthCard
The big clickable cards in the metric strip. Click to filter the
device table; click the info "i" to open the metric definition
panel. Border + ID color shift when active. */
function MetricHealthCard({ family, active, onClick, onInfoClick, onHover, onLeave }) {
const [h, setH] = useCompState(false);
const color = statusColor(family.worstStatus);
return (
<button
onClick={onClick}
onMouseEnter={(e) => { setH(true); onHover && onHover(e.currentTarget); }}
onMouseLeave={() => { setH(false); onLeave && onLeave(); }}
style={{
position: 'relative', textAlign: 'left', cursor: 'pointer',
background: active
? cAlpha(color, 0.15)
: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
border: `1.5px solid ${active ? color : (h ? cAlpha(color, 0.50) : cAlpha(color, 0.25))}`,
borderRadius: 8,
padding: '14px 16px',
minWidth: 160, flex: '1 1 0',
transition: 'all 160ms ease',
}}
>
<span
onClick={(e) => { e.stopPropagation(); onInfoClick && onInfoClick(family.metricId); }}
style={{
position: 'absolute', top: 8, right: 8,
display: 'inline-flex', cursor: 'pointer', padding: 2,
color: 'var(--fg-disabled)', borderRadius: 3,
}}
>
<CompIcon name="info" size={13} color="currentColor" />
</span>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 15, fontWeight: 700,
color: active ? color : 'var(--fg-1)', marginBottom: 4, paddingRight: 20,
}}>{family.metricId}</div>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 10,
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.05em',
marginBottom: 8,
}}>{family.category}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: 8 }}>
{family.entries.map((e, i) => (
<VariantPill
key={e.metric_id + '-' + i}
status={e.status} pct={e.compliance_pct}
label={family.entries.length > 1 ? (e.priority || `#${i + 1}`) : null}
/>
))}
</div>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 10,
color: 'var(--fg-disabled)', marginBottom: 8,
}}>target {pctDisplay(family.target)}</div>
<StatusRibbon status={family.worstStatus} />
</button>
);
}
/* MetricBadge
Compact category-tinted ID chip used in device-row "Failing Metrics"
columns and inside detail panels. */
function MetricBadge({ metricId, category }) {
const color = CATEGORY_COLORS[category] || C_COLORS.slate;
return (
<span style={{
display: 'inline-flex', alignItems: 'center',
padding: '2px 7px',
background: cAlpha(color, 0.12),
border: `1px solid ${cAlpha(color, 0.30)}`,
borderRadius: 3, color,
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
whiteSpace: 'nowrap',
}}>{metricId}</span>
);
}
/* SeenBadge
"1×" / "3×" / "5×" how many cycles a host has been failing the
same set of metrics. Color escalates: slate amber red. */
function SeenBadge({ count }) {
const color = count > 3 ? C_COLORS.red : count > 1 ? C_COLORS.amber : C_COLORS.slate;
return (
<span style={{
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 700,
color, padding: '2px 7px',
background: cAlpha(color, 0.10),
border: `1px solid ${cAlpha(color, 0.30)}`,
borderRadius: 3, whiteSpace: 'nowrap',
}}>{count}×</span>
);
}
/* DeviceTable + DeviceRow
The non-compliant host list. Toolbar has Active/Resolved tabs +
hostname search. Rows show hostname, IP, type, failing metric
badges, seen count, and a notes indicator. */
function DeviceTable({ children }) {
return (
<div style={{
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
border: '1px solid rgba(20,184,166,0.15)',
borderRadius: 8, overflow: 'hidden',
}}>{children}</div>
);
}
function DeviceTableToolbar({ tab, onTabChange, count, search, onSearchChange }) {
return (
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '14px 16px', borderBottom: '1px solid rgba(255,255,255,0.05)',
}}>
<div style={{ display: 'flex', gap: 4 }}>
{['active', 'resolved'].map(t => {
const on = tab === t;
return (
<button key={t} onClick={() => onTabChange(t)} style={{
padding: '6px 14px', cursor: 'pointer',
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.04em',
borderRadius: 4,
border: `1px solid ${on ? cAlpha(C_COLORS.teal, 0.40) : 'transparent'}`,
background: on ? cAlpha(C_COLORS.teal, 0.10) : 'transparent',
color: on ? C_COLORS.teal : 'var(--fg-disabled)',
}}>
{t}
{on && <span style={{ marginLeft: 6, color: 'var(--fg-2)' }}>({count})</span>}
</button>
);
})}
</div>
<CompSearchInput value={search} onChange={onSearchChange} placeholder="Search hostname…" width={220} />
</div>
);
}
function CompSearchInput({ value, onChange, placeholder, width = 240 }) {
const [focus, setFocus] = useCompState(false);
return (
<input
value={value} onChange={onChange} placeholder={placeholder}
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
style={{
background: 'rgba(15,23,42,0.85)',
border: `1px solid ${focus ? cAlpha(C_COLORS.teal, 0.60) : cAlpha(C_COLORS.teal, 0.20)}`,
borderRadius: 4, color: 'var(--fg-1)', outline: 'none',
padding: '6px 12px', fontSize: 12, fontFamily: 'var(--font-mono)',
width, transition: 'border-color 160ms ease',
boxShadow: focus ? `0 0 0 3px ${cAlpha(C_COLORS.teal, 0.10)}` : 'none',
}}
/>
);
}
const DEVICE_GRID = '2.5fr 1.1fr 1fr 2fr 0.5fr 0.4fr';
function DeviceTableHeader() {
return (
<div style={{
display: 'grid', gridTemplateColumns: DEVICE_GRID,
padding: '8px 16px',
borderBottom: '1px solid rgba(255,255,255,0.05)',
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.08em',
}}>
<span>Hostname</span><span>IP Address</span><span>Type</span>
<span>Failing Metrics</span><span>Seen</span><span></span>
</div>
);
}
function DeviceRow({ hostname, ip, type, failingMetrics, seenCount, hasNotes, selected, onClick }) {
const [hover, setHover] = useCompState(false);
return (
<div
onClick={onClick}
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
display: 'grid', gridTemplateColumns: DEVICE_GRID,
padding: '10px 16px',
borderBottom: '1px solid rgba(255,255,255,0.04)',
cursor: 'pointer',
background: selected ? cAlpha(C_COLORS.teal, 0.08) : (hover ? 'rgba(255,255,255,0.025)' : 'transparent'),
borderLeft: selected ? `2px solid ${C_COLORS.teal}` : '2px solid transparent',
transition: 'all 160ms ease', alignItems: 'center',
}}
>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 12,
color: selected ? C_COLORS.teal : 'var(--fg-1)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{hostname}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)' }}>{ip || '—'}</div>
<div style={{ fontSize: 11, color: 'var(--fg-disabled)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{type || '—'}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
{failingMetrics.map(m => <MetricBadge key={m.metric_id} metricId={m.metric_id} category={m.category} />)}
</div>
<div><SeenBadge count={seenCount} /></div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
{hasNotes && <CompIcon name="message" size={13} color={cAlpha(C_COLORS.teal, 0.7)} />}
</div>
</div>
);
}
/* ── EmptyState — for table body when there's nothing to show. ── */
function CompEmpty({ children }) {
return (
<div style={{
padding: 48, textAlign: 'center',
color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', fontSize: 12,
}}>{children}</div>
);
}
/* ── ChartCard — wrapper around any of the 6 charts on the page. ── */
function ChartCard({ title, subtitle, children, height = 240 }) {
return (
<div style={{
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
border: '1px solid rgba(20,184,166,0.15)',
borderRadius: 8, padding: 16,
}}>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
color: 'var(--fg-1)', textTransform: 'uppercase', letterSpacing: '0.08em',
marginBottom: subtitle ? 2 : 12,
}}>{title}</div>
{subtitle && (
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 10,
color: 'var(--fg-disabled)', marginBottom: 12,
}}>{subtitle}</div>
)}
<div style={{ height }}>{children}</div>
</div>
);
}
/* ── ChartLegend — shared legend row used at the top of stacked charts. ── */
function ChartLegend({ items }) {
return (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 8 }}>
{items.map(it => (
<span key={it.label} style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-2)',
}}>
<span style={{
display: 'inline-block', width: 10, height: 10, borderRadius: 2,
background: it.color,
}} />
{it.label}
</span>
))}
</div>
);
}
/* DefinitionTooltip
The hover popover that surfaces a metric's title + business
justification + data sources. */
function DefinitionTooltip({ title, justification, sources }) {
return (
<div style={{
width: 300,
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
border: `1px solid ${cAlpha(C_COLORS.teal, 0.30)}`,
borderRadius: 8,
boxShadow: '0 8px 32px rgba(0,0,0,0.6)',
padding: '12px 14px',
}}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 700, color: 'var(--fg-1)', marginBottom: 6, lineHeight: 1.3 }}>{title}</div>
{justification && (
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-2)', lineHeight: 1.4, marginBottom: 6 }}>{justification}</div>
)}
{sources && (
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-disabled)' }}>Sources: {sources}</div>
)}
</div>
);
}
/* RollbackDialog
Centered modal w/ red identity. "Reverses the most recent upload"
message + danger confirm. */
function RollbackDialog({ reportLabel, onCancel, onConfirm, loading }) {
return (
<div style={{
position: 'absolute', inset: 0, zIndex: 60,
background: 'rgba(10,14,39,0.92)', backdropFilter: 'blur(8px)',
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
}}>
<div style={{
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
border: '1px solid rgba(239,68,68,0.30)', borderRadius: 12,
boxShadow: '0 20px 60px rgba(0,0,0,0.7)',
width: '100%', maxWidth: 420, padding: 28,
}}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 15, fontWeight: 700, color: C_COLORS.red, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 12 }}>
Rollback Upload
</div>
<div style={{ fontSize: 13, color: 'var(--fg-1)', lineHeight: 1.5, marginBottom: 8, fontFamily: 'var(--font-display)' }}>
This will reverse the most recent upload:
</div>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)',
background: 'rgba(15,23,42,0.6)', borderRadius: 6,
padding: '10px 12px', marginBottom: 18,
border: '1px solid rgba(239,68,68,0.15)',
}}>
<div><span style={{ color: 'var(--fg-disabled)' }}>File:</span> {reportLabel}</div>
<div style={{ marginTop: 4, fontSize: 10, color: 'var(--fg-disabled)' }}>
New items will be deleted, resolved items will be reactivated, and the upload record will be removed.
</div>
</div>
<div style={{ display: 'flex', gap: 10 }}>
<CompButton variant="ghost" onClick={onCancel} style={{ flex: 1, justifyContent: 'center' }}>Cancel</CompButton>
<button onClick={onConfirm} disabled={loading} style={{
flex: 2, padding: 10,
background: loading ? 'rgba(239,68,68,0.05)' : 'rgba(239,68,68,0.10)',
border: `1px solid ${C_COLORS.red}`, borderRadius: 6,
color: C_COLORS.red, cursor: loading ? 'wait' : 'pointer',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
opacity: loading ? 0.6 : 1,
}}>
<CompIcon name="rotate" size={13} color="currentColor" />
{loading ? 'Rolling back…' : 'Confirm Rollback'}
</button>
</div>
</div>
</div>
);
}
/* ── RollbackToast — bottom-right confirmation/error toast. ── */
function RollbackToast({ tone = 'success', message, detail, onDismiss }) {
const c = tone === 'error' ? C_COLORS.red : C_COLORS.green;
return (
<div onClick={onDismiss} style={{
position: 'absolute', bottom: 24, right: 24, zIndex: 70,
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
border: `1px solid ${cAlpha(c, 0.40)}`, borderRadius: 8,
boxShadow: '0 8px 32px rgba(0,0,0,0.5)',
padding: '14px 20px', maxWidth: 360,
fontFamily: 'var(--font-mono)', fontSize: 12, color: c, cursor: 'pointer',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: detail ? 4 : 0 }}>
<CompIcon name={tone === 'error' ? 'alert' : 'rotate'} size={14} color="currentColor" />
{message}
</div>
{detail && <div style={{ fontSize: 10, color: 'var(--fg-2)' }}>{detail}</div>}
</div>
);
}
/* ── CompIcon — every icon used by the compliance page. ── */
function CompIcon({ name, size = 16, color = 'currentColor' }) {
const p = {
width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
stroke: color, strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
style: { display: 'inline-block', verticalAlign: 'middle' },
};
switch (name) {
case 'upload': return <svg {...p}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>;
case 'refresh': return <svg {...p}><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/><path d="M3 21v-5h5"/></svg>;
case 'rotate': return <svg {...p}><path d="M3 12a9 9 0 1 0 9-9"/><polyline points="3 4 3 9 8 9"/></svg>;
case 'message': return <svg {...p}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>;
case 'info': return <svg {...p}><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>;
case 'alert': return <svg {...p}><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>;
case 'check': return <svg {...p}><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>;
case 'loader': return <svg {...p}><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/></svg>;
case 'x': return <svg {...p}><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>;
default: return <svg {...p}><circle cx="12" cy="12" r="10"/></svg>;
}
}
window.COMP = {
COLORS: C_COLORS, STATUS_COLOR, CATEGORY_COLORS,
statusColor, pctDisplay, cAlpha,
CompPageHeader, CompButton, CompIconButton, TeamTabs,
VariantPill, StatusRibbon, MetricHealthCard, MetricBadge, SeenBadge,
DeviceTable, DeviceTableToolbar, DeviceTableHeader, DeviceRow, CompSearchInput, CompEmpty,
ChartCard, ChartLegend,
DefinitionTooltip, RollbackDialog, RollbackToast,
CompIcon,
};