// ReportPrimitives.jsx — Reporting-specific UI vocabulary.
// All inline styles + tokens from ../../colors_and_type.css.
// Mirrors the live Reporting page (frontend/src/components/pages/ReportingPage.js)
// after the Knowledge-Base alignment pass.
const { useState: useRPTState } = React;
/* ─────────────────────────────────────────────────────────────────
COLOR ROLE MAP (Reporting)
──────────────────────────────────────────────────────────────────
Sky-blue (#0EA5E9) → primary surface accent (panel borders,
tab pill active, donut highlight, table
header text, neutral secondary buttons)
Green (#10B981) → page identity (header glow + primary
Sync button)
Amber (#F59E0B) → filter-active indicator, anomaly callout
Red (#EF4444) → error / overdue
Slate stack → muted text + dividers (#475569 → #334155)
──────────────────────────────────────────────────────────────── */
const COLORS = {
sky: '#0EA5E9',
skySoft: '#7DD3FC',
green: '#10B981',
amber: '#F59E0B',
red: '#EF4444',
redSoft: '#FCA5A5',
};
/* ── Page header ─────────────────────────────────────────────────
Big mono uppercase title in green w/ glow + count subtitle.
Right side: neutral icon-tinted secondaries + tinted-fill primary.
Lifted from the existing Knowledge Base page header pattern. */
function PageHeader({ title = 'Reporting', meta, children }) {
return (
{title}
{meta && (
{meta}
)}
{children}
);
}
/* ── Buttons ─────────────────────────────────────────────────────
THREE variants documented for Reporting:
• primary — green tinted-fill (lone primary action: Sync)
• neutral — sky outlined transparent (Atlas, refresh, etc.)
• subtle — sky tinted-fill (Export, Queue, Column manager)
*/
function RptButton({ variant = 'neutral', icon, children, disabled, ...rest }) {
const [hover, setHover] = useRPTState(false);
const v = {
primary: {
bgRest: 'rgba(16,185,129,0.18)',
bgHover: 'rgba(16,185,129,0.26)',
bd: COLORS.green, fg: COLORS.green,
},
neutral: {
bgRest: 'transparent',
bgHover: 'rgba(14,165,233,0.06)',
bd: 'rgba(14,165,233,0.25)', fg: COLORS.sky,
},
subtle: {
bgRest: 'rgba(14,165,233,0.08)',
bgHover: 'rgba(14,165,233,0.16)',
bd: 'rgba(14,165,233,0.35)', fg: COLORS.sky,
},
danger: {
bgRest: 'rgba(239,68,68,0.08)',
bgHover: 'rgba(239,68,68,0.16)',
bd: 'rgba(239,68,68,0.30)', fg: COLORS.red,
},
}[variant];
return (
);
}
/* ── KB-style card (sky) — used for donuts + findings panel ──── */
function KbCard({ children, padding = 16, label, labelExtra, hover = true, style }) {
const [h, setH] = useRPTState(false);
return (
hover && setH(true)} onMouseLeave={() => setH(false)}
style={{
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
border: `1.5px solid ${h ? 'rgba(14,165,233,0.35)' : 'rgba(14,165,233,0.12)'}`,
borderRadius: 8, padding,
display: 'flex', flexDirection: 'column', gap: 10,
transition: 'border-color 150ms cubic-bezier(0.4,0,0.2,1)',
...style,
}}>
{label && (
{label}
{labelExtra}
)}
{children}
);
}
/* ── Pill tab (Ivanti / Atlas) ───────────────────────────────── */
function PillTab({ active, color = COLORS.sky, onClick, children }) {
const [hover, setHover] = useRPTState(false);
return (
);
}
/* ── Filter chip (active filter pin in the toolbar) ──────────── */
function FilterChip({ color = COLORS.amber, onClear, children }) {
return (
);
}
/* ── Status banner (error / Atlas error / sync error) ────────── */
function StatusBanner({ tone = 'error', children }) {
const tones = {
error: { bg: 'rgba(239,68,68,0.08)', bd: 'rgba(239,68,68,0.25)', fg: COLORS.redSoft, icon: COLORS.red },
warn: { bg: 'rgba(245,158,11,0.10)', bd: 'rgba(245,158,11,0.28)', fg: '#FCD34D', icon: COLORS.amber },
info: { bg: 'rgba(14,165,233,0.08)', bd: 'rgba(14,165,233,0.25)', fg: COLORS.skySoft, icon: COLORS.sky },
};
const t = tones[tone];
return (
{children}
);
}
/* ── Toolbar label (small mono uppercase, used inside findings panel) ── */
function ToolbarLabel({ children, accent = COLORS.sky, count }) {
return (
{children}
{count != null && (
{count}
)}
);
}
/* ── Severity dot (used in table rows) ───────────────────────── */
function SeverityDot({ level }) {
const map = {
Critical: { c: COLORS.red, text: '#FCA5A5' },
High: { c: COLORS.amber, text: '#FCD34D' },
Medium: { c: COLORS.sky, text: '#7DD3FC' },
Low: { c: COLORS.green, text: '#6EE7B7' },
Info: { c: '#94A3B8', text: '#CBD5E1' },
};
const v = map[level] || map.Info;
return (
{level}
);
}
/* ── SLA pill (table cell) ───────────────────────────────────── */
function SlaPill({ status }) {
const map = {
OVERDUE: { c: COLORS.red, bg: 'rgba(239,68,68,0.16)' },
AT_RISK: { c: COLORS.amber, bg: 'rgba(245,158,11,0.16)' },
WITHIN_SLA: { c: COLORS.green, bg: 'rgba(16,185,129,0.16)' },
};
const v = map[status] || map.WITHIN_SLA;
return (
{status.replace('_', ' ')}
);
}
/* ── Workflow badge (table cell) ─────────────────────────────── */
function WorkflowBadge({ state }) {
const map = {
OPEN: { c: COLORS.sky, bg: 'rgba(14,165,233,0.14)' },
FP: { c: COLORS.amber, bg: 'rgba(245,158,11,0.14)' },
EXC: { c: '#A78BFA', bg: 'rgba(167,139,250,0.14)' },
REMEDIATED:{ c: COLORS.green, bg: 'rgba(16,185,129,0.14)' },
ARCHIVED: { c: '#94A3B8', bg: 'rgba(148,163,184,0.14)' },
};
const v = map[state] || { c: 'var(--fg-muted)', bg: 'rgba(148,163,184,0.10)' };
return (
{state}
);
}
/* ── Donut placeholder — semantic stand-in for the real recharts donut ── */
function DonutSample({ size = 130, segments, centerLabel, centerValue }) {
// segments: [{ label, value, color }]
const total = segments.reduce((s, x) => s + x.value, 0);
const cx = size / 2, cy = size / 2;
const outerR = size / 2 - 4, innerR = outerR - 16;
let angle = -90;
const arcs = segments.map((seg) => {
const sweep = (seg.value / total) * 360;
const a0 = (angle * Math.PI) / 180;
const a1 = ((angle + sweep) * Math.PI) / 180;
const large = sweep > 180 ? 1 : 0;
const x0 = cx + outerR * Math.cos(a0), y0 = cy + outerR * Math.sin(a0);
const x1 = cx + outerR * Math.cos(a1), y1 = cy + outerR * Math.sin(a1);
const xi1 = cx + innerR * Math.cos(a1), yi1 = cy + innerR * Math.sin(a1);
const xi0 = cx + innerR * Math.cos(a0), yi0 = cy + innerR * Math.sin(a0);
const d = `M ${x0} ${y0} A ${outerR} ${outerR} 0 ${large} 1 ${x1} ${y1}
L ${xi1} ${yi1} A ${innerR} ${innerR} 0 ${large} 0 ${xi0} ${yi0} Z`;
angle += sweep;
return { d, color: seg.color };
});
return (
{centerValue}
{centerLabel && (
{centerLabel}
)}
{/* Legend */}
{segments.map((s) => (
{s.label} {s.value}
))}
);
}
/* ── Inline lucide icons (Reporting subset) ──────────────────── */
const _ic = (path) => ({ size = 14, strokeWidth = 1.75, ...rest }) => (
);
const RptIcon = {
Refresh: _ic(<>>),
PieChart: _ic(<>>),
Filter: _ic(<>>),
Download: _ic(<>>),
ChevronD: _ic(<>>),
ChevronUp: _ic(<>>),
ChevronUpDn:_ic(<>>),
ListTodo: _ic(<>>),
Settings: _ic(<>>),
Eye: _ic(<>>),
EyeOff: _ic(<>>),
AlertCircle:_ic(<>>),
AlertTri: _ic(<>>),
Atlas: _ic(<>>),
Search: _ic(<>>),
Square: _ic(<>>),
CheckSq: _ic(<>>),
Loader: _ic(<>>),
TrendUp: _ic(<>>),
};
window.RPT = {
COLORS,
PageHeader, RptButton, KbCard, PillTab, FilterChip, StatusBanner,
ToolbarLabel, SeverityDot, SlaPill, WorkflowBadge, DonutSample,
RptIcon,
};