86 lines
2.4 KiB
TypeScript
86 lines
2.4 KiB
TypeScript
import { cn } from "@/lib/utils";
|
||
|
||
function num(v: number | string | null | undefined): number | null {
|
||
if (v == null || v === "") return null;
|
||
const n = typeof v === "string" ? Number(v) : v;
|
||
return Number.isFinite(n) ? n : null;
|
||
}
|
||
|
||
function pct(v: number | string | null | undefined): number {
|
||
const n = num(v);
|
||
if (n == null) return 0;
|
||
return Math.max(0, Math.min(5, n)) / 5 * 100;
|
||
}
|
||
|
||
export function MiniBars({
|
||
usefulness,
|
||
usability,
|
||
className,
|
||
}: {
|
||
usefulness: number | string | null | undefined;
|
||
usability: number | string | null | undefined;
|
||
className?: string;
|
||
}) {
|
||
const rows = [
|
||
{ label: "Usefulness", value: usefulness },
|
||
{ label: "Usability", value: usability },
|
||
];
|
||
return (
|
||
<div className={cn("space-y-1 w-full", className)}>
|
||
{rows.map(({ label, value }) => {
|
||
const n = num(value);
|
||
return (
|
||
<div
|
||
key={label}
|
||
className="flex items-center gap-2"
|
||
title={`${label}: ${n != null ? n.toFixed(1) : "N/A"} / 5`}
|
||
>
|
||
<span className="text-[10px] uppercase tracking-wide text-muted-foreground w-[3.5rem] shrink-0">
|
||
{label}
|
||
</span>
|
||
<div className="flex-1 h-1.5 rounded-full bg-foreground/5 overflow-hidden">
|
||
<div
|
||
className="neon-bar h-full rounded-full"
|
||
style={{ width: `${pct(value)}%` }}
|
||
/>
|
||
</div>
|
||
<span className="text-[10px] text-muted-foreground tabular-nums w-7 text-right shrink-0">
|
||
{n != null ? n.toFixed(1) : "–"}
|
||
</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function MiniBarStack({
|
||
usefulness,
|
||
usability,
|
||
className,
|
||
}: {
|
||
usefulness: number | string | null | undefined;
|
||
usability: number | string | null | undefined;
|
||
className?: string;
|
||
}) {
|
||
const u = num(usefulness);
|
||
const a = num(usability);
|
||
return (
|
||
<div
|
||
className={cn("h-1.5 w-16 rounded-full bg-foreground/5 overflow-hidden flex", className)}
|
||
title={`Usefulness ${u != null ? u.toFixed(1) : "N/A"} / Usability ${
|
||
a != null ? a.toFixed(1) : "N/A"
|
||
} (of 5)`}
|
||
>
|
||
<div
|
||
className="neon-bar h-full"
|
||
style={{ width: `${pct(usefulness)}%` }}
|
||
/>
|
||
<div
|
||
className="neon-bar h-full opacity-60"
|
||
style={{ width: `${pct(usability)}%` }}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|