update applying current ai insight from dashboard-cpsp
This commit is contained in:
1 parent
31272035e9
commit
b87f20e3b3
27 files changed
+5300
-343
No files matched your search
@@ -0,0 +1,169 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Headline figures for AI Insight cards.
|
||||
* Matches NEW dashboard gray card patterns (white + border-gray-100).
|
||||
*/
|
||||
|
||||
export type InsightStatus = 'ok' | 'warning' | 'critical' | 'unknown';
|
||||
|
||||
const STATUS_CHIP: Record<InsightStatus, { cls: string; glyph: string }> = {
|
||||
ok: { cls: 'bg-green-50 text-green-800 border-green-200', glyph: '✓' },
|
||||
warning: { cls: 'bg-amber-50 text-amber-800 border-amber-200', glyph: '!' },
|
||||
critical: { cls: 'bg-red-50 text-red-800 border-red-200', glyph: '⚠' },
|
||||
unknown: { cls: 'bg-gray-50 text-gray-500 border-gray-200', glyph: '–' },
|
||||
};
|
||||
|
||||
interface DeltaChipProps {
|
||||
status: InsightStatus;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const DeltaChip: React.FC<DeltaChipProps> = ({ status, label }) => {
|
||||
const chip = STATUS_CHIP[status];
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold ${chip.cls}`}
|
||||
>
|
||||
<span aria-hidden="true">{chip.glyph}</span>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
/** Axis-less sparkline — shape only; the tile number carries the value. */
|
||||
export const Sparkline: React.FC<{ values: number[]; className?: string }> = ({
|
||||
values,
|
||||
className,
|
||||
}) => {
|
||||
const clean = values.filter((v) => typeof v === 'number' && Number.isFinite(v));
|
||||
if (clean.length < 2) return null;
|
||||
|
||||
const width = 88;
|
||||
const height = 28;
|
||||
const min = Math.min(...clean);
|
||||
const max = Math.max(...clean);
|
||||
const span = max - min || 1;
|
||||
const step = width / (clean.length - 1);
|
||||
const points = clean
|
||||
.map((value, index) => {
|
||||
const x = index * step;
|
||||
const y = height - 2 - ((value - min) / span) * (height - 4);
|
||||
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<polyline
|
||||
points={points}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
interface HeroFigureProps {
|
||||
label: string;
|
||||
value: string;
|
||||
unit?: string;
|
||||
context?: string | null;
|
||||
status?: InsightStatus;
|
||||
deltaLabel?: string | null;
|
||||
sparkline?: number[];
|
||||
}
|
||||
|
||||
export const HeroFigure: React.FC<HeroFigureProps> = ({
|
||||
label,
|
||||
value,
|
||||
unit,
|
||||
context,
|
||||
status = 'unknown',
|
||||
deltaLabel,
|
||||
sparkline,
|
||||
}) => (
|
||||
<div className="flex items-end justify-between gap-4 rounded-xl border border-gray-100 bg-white px-5 py-4 shadow-sm">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500">{label}</div>
|
||||
<div className="mt-1 flex items-baseline gap-1.5 pb-0.5">
|
||||
<span className="text-5xl font-bold leading-[1.15] tracking-tight text-gray-900">
|
||||
{value}
|
||||
</span>
|
||||
{unit ? <span className="text-sm font-medium text-gray-500">{unit}</span> : null}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
{deltaLabel ? <DeltaChip status={status} label={deltaLabel} /> : null}
|
||||
{context ? <span className="text-xs text-gray-500">{context}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
{sparkline && sparkline.length > 1 ? (
|
||||
<Sparkline values={sparkline} className="shrink-0 text-red-400" />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface StatTileProps {
|
||||
label: string;
|
||||
value: string;
|
||||
unit?: string;
|
||||
hint?: string | null;
|
||||
status?: InsightStatus;
|
||||
deltaLabel?: string | null;
|
||||
sparkline?: number[];
|
||||
}
|
||||
|
||||
export const StatTile: React.FC<StatTileProps> = ({
|
||||
label,
|
||||
value,
|
||||
unit,
|
||||
hint,
|
||||
status = 'unknown',
|
||||
deltaLabel,
|
||||
sparkline,
|
||||
}) => (
|
||||
<div className="rounded-xl border border-gray-100 bg-white px-4 py-3 shadow-sm">
|
||||
<div className="text-xs font-medium text-gray-500">
|
||||
{label}
|
||||
{hint ? <span className="ml-1 text-[11px] text-gray-400">{hint}</span> : null}
|
||||
</div>
|
||||
<div className="mt-1 flex items-baseline gap-1">
|
||||
<span className="text-xl font-bold leading-[1.3] text-gray-900">{value}</span>
|
||||
{unit ? <span className="text-xs font-medium text-gray-500">{unit}</span> : null}
|
||||
</div>
|
||||
{deltaLabel ? (
|
||||
<div className="mt-1.5">
|
||||
<DeltaChip status={status} label={deltaLabel} />
|
||||
</div>
|
||||
) : null}
|
||||
{sparkline && sparkline.length > 1 ? (
|
||||
<Sparkline values={sparkline} className="mt-2 text-gray-300" />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
/** Secondary figures: subordinate to hero / tiles. */
|
||||
export const MutedStatRow: React.FC<{ label: string; hint?: string | null; value: string }> = ({
|
||||
label,
|
||||
hint,
|
||||
value,
|
||||
}) => (
|
||||
<div className="flex items-center justify-between border-b border-gray-50 py-1.5">
|
||||
<span className="text-sm text-gray-600">
|
||||
{label}
|
||||
{hint ? <span className="ml-1 text-xs text-gray-400">{hint}</span> : null}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-gray-900">{value}</span>
|
||||
</div>
|
||||
);
|
||||
Reference in new issue
Block a user