// Primitives.jsx — KpiCard, LineChart, BarChart, DonutChart, HeatmapChart, Alert function KpiCard({ label, value, sub, delta, deltaDir, icon, variant, accentColor }) { const isDanger = variant === 'danger'; const isBrand = variant === 'brand'; const isWarning = variant === 'warning'; const isSuccess = variant === 'success'; const topBorder = isDanger ? '3px solid var(--danger-500)' : isWarning ? '3px solid var(--warning-500)' : isSuccess ? '3px solid var(--success-500)' : isBrand ? 'none' : '1px solid var(--border-subtle)'; const bg = isBrand ? 'var(--brand-gradient)' : '#fff'; const fg = isBrand ? '#fff' : 'var(--fg-1)'; const fgMuted = isBrand ? 'rgba(255,255,255,0.8)' : 'var(--fg-3)'; const deltaUp = deltaDir === 'up'; const deltaDn = deltaDir === 'down'; return (
{label} {icon && }
{value}
{sub &&
{sub}
} {delta && (
{deltaUp ? '↑ ' : deltaDn ? '↓ ' : ''}{delta}
)}
); } function LineChart({ series, labels, height = 200 }) { const W = 700, H = height; const pad = { l: 40, r: 16, t: 12, b: 28 }; const all = series.flatMap(s => s.data); const maxV = Math.max(...all) * 1.12; const minV = 0; const n = series[0].data.length; const xStep = (W - pad.l - pad.r) / Math.max(n - 1, 1); const yFn = v => H - pad.b - ((v - minV) / (maxV - minV || 1)) * (H - pad.t - pad.b); const xFn = i => pad.l + i * xStep; const gridVals = [0, 0.25, 0.5, 0.75, 1].map(t => Math.round(minV + (maxV - minV) * t)); return ( {gridVals.map((g, i) => ( {g} ))} {labels && labels.map((lab, i) => ( {lab} ))} {series.map((s, si) => { const pts = s.data.map((v, i) => `${xFn(i)},${yFn(v)}`); const areaD = `M ${xFn(0)},${yFn(0)} L ${pts.join(' L ')} L ${xFn(n - 1)},${yFn(0)} Z`; return ( {si === 0 && } {s.data.map((v, i) => )} ); })} ); } function BarChart({ data, height = 200, horizontal = false }) { if (horizontal) { const W = 360, H = Math.max(height, data.length * 36 + 20); const pad = { l: 140, r: 40, t: 8, b: 8 }; const maxV = Math.max(...data.map(d => d.value)) * 1.1; const barH = 18; const step = (H - pad.t - pad.b) / data.length; return ( {data.map((d, i) => { const y = pad.t + i * step + (step - barH) / 2; const bw = ((d.value / maxV) * (W - pad.l - pad.r)); const color = d.color || 'var(--primary-600)'; return ( {d.label} {d.value} ); })} ); } const W = 400, H = height; const pad = { l: 36, r: 12, t: 12, b: 32 }; const maxV = Math.max(...data.map(d => d.value)) * 1.15; const bw = (W - pad.l - pad.r) / data.length * 0.62; const sw = (W - pad.l - pad.r) / data.length; const yFn = v => H - pad.b - (v / (maxV || 1)) * (H - pad.t - pad.b); return ( {[0, 0.5, 1].map((t, i) => { const val = maxV * t; return ( {Math.round(val)} ); })} {data.map((d, i) => { const cx = pad.l + sw * i + sw / 2; const color = d.color || 'var(--primary-600)'; const barTop = yFn(d.value); return ( {d.label} ); })} ); } function DonutChart({ segments, size = 160, label, sub }) { const total = segments.reduce((a, s) => a + s.value, 0); let angle = -Math.PI / 2; const cx = size / 2, cy = size / 2; const R = size * 0.38, r = size * 0.24; const paths = segments.map(seg => { const sweep = (seg.value / total) * Math.PI * 2; const x1 = cx + R * Math.cos(angle); const y1 = cy + R * Math.sin(angle); const x2 = cx + R * Math.cos(angle + sweep); const y2 = cy + R * Math.sin(angle + sweep); const xi1 = cx + r * Math.cos(angle); const yi1 = cy + r * Math.sin(angle); const xi2 = cx + r * Math.cos(angle + sweep); const yi2 = cy + r * Math.sin(angle + sweep); const large = sweep > Math.PI ? 1 : 0; const d = `M ${xi1} ${yi1} L ${x1} ${y1} A ${R} ${R} 0 ${large} 1 ${x2} ${y2} L ${xi2} ${yi2} A ${r} ${r} 0 ${large} 0 ${xi1} ${yi1} Z`; angle += sweep; return { d, color: seg.color }; }); return ( {paths.map((p, i) => )} {label} {sub && {sub}} ); } function HeatmapChart({ data, days, hours }) { // data: array of {day, hour, value} const cellW = 28, cellH = 22, padL = 52, padT = 28; const W = padL + hours.length * cellW + 10; const H = padT + days.length * cellH + 10; const maxV = Math.max(...data.map(d => d.value)); function cellColor(v) { if (v === 0) return 'var(--gray-100)'; const t = v / maxV; if (t < 0.2) return '#e9d5f5'; if (t < 0.4) return '#c084d4'; if (t < 0.6) return '#9a3f90'; if (t < 0.8) return '#7d2574'; return '#4a1545'; } return ( {hours.map((h, hi) => ( {h} ))} {days.map((d, di) => ( {d} ))} {data.map((cell, i) => { const di = days.indexOf(cell.day); const hi = hours.indexOf(cell.hour); if (di < 0 || hi < 0) return null; return ( ); })} ); } function Alert({ level, title, body, action }) { const cfg = { danger: { bg: 'var(--danger-50)', border: 'var(--danger-200)', icon: 'alert-triangle', color: 'var(--danger-700)', iconBg: 'var(--danger-100)' }, warning: { bg: 'var(--warning-50)', border: 'var(--warning-200)', icon: 'alert-circle', color: 'var(--warning-700)', iconBg: 'var(--warning-100)' }, info: { bg: 'var(--info-50)', border: 'var(--info-100)', icon: 'info', color: 'var(--info-700)', iconBg: 'var(--info-100)' }, success: { bg: 'var(--success-50)', border: 'var(--success-100)', icon: 'check-circle-2', color: 'var(--success-700)', iconBg: 'var(--success-100)' }, reco: { bg: 'var(--primary-50)', border: 'var(--primary-200)', icon: 'sparkles', color: 'var(--primary-700)', iconBg: 'var(--primary-100)' }, }[level] || {}; return (
{title}
{body &&
{body}
} {action && ( )}
); } function SectionCard({ title, subtitle, children, style, action }) { return (
{title}
{subtitle &&
{subtitle}
}
{action}
{children}
); } function Badge({ label, color = 'var(--primary-600)', bg = 'var(--primary-50)' }) { return ( {label} ); } function StatRow({ label, value, sub, bar, barColor = 'var(--primary-500)', total }) { const pct = total ? (value / total) * 100 : bar; return (
{label} {typeof value === 'number' ? value.toLocaleString('es-AR') : value}
{sub &&
{sub}
} {pct !== undefined && (
)}
); } Object.assign(window, { KpiCard, LineChart, BarChart, DonutChart, HeatmapChart, Alert, SectionCard, Badge, StatRow });