Pipeline/frontend/src/components/claude/RankedAnalyticsList.tsx

112 lines
4.1 KiB
TypeScript

"use client";
import { useState } from "react";
import { Check, Clipboard, FileCode2, Terminal } from "lucide-react";
import { cn } from "@/lib/utils";
export type RankedAnalyticsItem = {
label: string;
count: number;
};
type RankedAnalyticsListProps = {
title: string;
items: RankedAnalyticsItem[];
kind: "file" | "command";
};
async function copyValue(value: string, onCopied: () => void) {
if (!value || typeof navigator === "undefined" || !navigator.clipboard)
return;
await navigator.clipboard.writeText(value);
onCopied();
}
export function RankedAnalyticsList({
title,
items,
kind,
}: RankedAnalyticsListProps) {
const [copied, setCopied] = useState<string | null>(null);
const Icon = kind === "command" ? Terminal : FileCode2;
const markCopied = (value: string) => {
setCopied(value);
window.setTimeout(() => setCopied(null), 1400);
};
return (
<section
className="min-w-0 rounded-2xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-sm"
aria-labelledby={`${title.toLowerCase().replace(/\s+/g, "-")}-heading`}
>
<div className="mb-4 flex items-center justify-between gap-3">
<div className="min-w-0">
<h3
id={`${title.toLowerCase().replace(/\s+/g, "-")}-heading`}
className="truncate text-sm font-semibold text-[color:var(--text)]"
>
{title}
</h3>
<p className="mt-1 text-xs text-[color:var(--text-muted)]">
Top {items.length.toLocaleString()} by count
</p>
</div>
<span
className={cn(
"flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ring-1",
kind === "command"
? "bg-amber-500/15 text-amber-300 ring-amber-400/20"
: "bg-violet-500/15 text-violet-300 ring-violet-400/20",
)}
>
<Icon className="h-4 w-4" />
</span>
</div>
{items.length === 0 ? (
<div className="rounded-xl border border-dashed border-[color:var(--border-strong)] p-5 text-sm text-[color:var(--text-muted)]">
No entries in this range.
</div>
) : (
<ol className="space-y-2">
{items.map((item, index) => (
<li
key={`${item.label}-${index}`}
className="group flex min-w-0 items-center gap-3 rounded-xl border border-[color:var(--border)] bg-[color:var(--bg)] px-3 py-2.5 transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-muted)]"
>
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-[color:var(--surface-muted)] text-xs font-bold text-[color:var(--text-muted)]">
{index + 1}
</span>
<span
className="min-w-0 flex-1 truncate font-mono text-xs text-[color:var(--text)]"
title={item.label}
>
{item.label}
</span>
<span className="shrink-0 rounded-full bg-[color:var(--accent-soft)] px-2 py-1 text-xs font-semibold text-[color:var(--accent-strong)]">
{item.count.toLocaleString()}
</span>
<button
type="button"
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-[color:var(--border)] text-[color:var(--text-muted)] opacity-100 transition hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)] md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
aria-label={`Copy ${item.label}`}
onClick={() =>
copyValue(item.label, () => markCopied(item.label))
}
>
{copied === item.label ? (
<Check className="h-3.5 w-3.5" />
) : (
<Clipboard className="h-3.5 w-3.5" />
)}
</button>
</li>
))}
</ol>
)}
</section>
);
}