spb/social-runtime-crawler
Public
TypeScript 91.8%
HTML 3.2%
JavaScript 3%
SQL 1.4%
CSS 0.7%
1"use client";23import clsx from "clsx";4import type { ReactNode } from "react";5import { TYPE_COLORS } from "@/lib/format";67export function Panel({ title, sub, right, children, className, pad = true }: { title?: ReactNode; sub?: ReactNode; right?: ReactNode; children: ReactNode; className?: string; pad?: boolean }) {8 return (9 <section className={clsx("panel flex flex-col min-w-0", className)}>10 {(title || right) && (11 <header className="flex items-center gap-3 px-4 py-3 border-b border-line">12 <div className="min-w-0">13 {title && <h2 className="text-[11px] uppercase tracking-[0.18em] text-fg-2 font-medium">{title}</h2>}14 {sub && <p className="text-xs text-dim truncate">{sub}</p>}15 </div>16 {right && <div className="ml-auto shrink-0 text-xs text-dim">{right}</div>}17 </header>18 )}19 <div className={clsx("min-w-0 flex-1", pad && "p-4")}>{children}</div>20 </section>21 );22}2324export function Kpi({ label, value, hint, accent = "cyan", spark }: { label: string; value: ReactNode; hint?: ReactNode; accent?: "cyan" | "amber" | "green" | "violet" | "pink" | "red" | "fg"; spark?: number[] }) {25 const color = { cyan: "text-cyan", amber: "text-amber", green: "text-green", violet: "text-violet", pink: "text-pink", red: "text-red", fg: "text-fg" }[accent];26 return (27 <div className="panel p-4 relative overflow-hidden">28 <div className="text-[11px] uppercase tracking-[0.18em] text-dim">{label}</div>29 <div className={clsx("mono text-3xl font-semibold mt-1 tabular-nums", color)}>{value}</div>30 {hint && <div className="text-xs text-fg-2 mt-1">{hint}</div>}31 {spark && spark.length > 1 && <Spark values={spark} className="absolute right-3 bottom-3 h-8 w-24 opacity-70" />}32 </div>33 );34}3536export function Spark({ values, className, color = "currentColor" }: { values: number[]; className?: string; color?: string }) {37 const max = Math.max(1, ...values);38 const w = 100;39 const h = 30;40 const pts = values.map((v, i) => `${(i / Math.max(1, values.length - 1)) * w},${h - (v / max) * (h - 2) - 1}`).join(" ");41 return (42 <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" className={className}>43 <polyline points={pts} fill="none" stroke={color} strokeWidth="1.5" vectorEffect="non-scaling-stroke" />44 </svg>45 );46}4748export function Tag({ children, color, className, title }: { children: ReactNode; color?: string; className?: string; title?: string }) {49 const c = color ?? "#6b7a8c";50 return (51 <span title={title} className={clsx("inline-flex items-center rounded-md px-1.5 py-0.5 text-[10.5px] font-medium mono leading-4 whitespace-nowrap", className)} style={{ color: c, background: `${c}1f`, border: `1px solid ${c}33` }}>52 {children}53 </span>54 );55}5657export function TypeTag({ type }: { type: string }) {58 return <Tag color={TYPE_COLORS[type] ?? "#6b7a8c"}>{type}</Tag>;59}6061export function SurfaceChips({ provenance }: { provenance?: { surface: string; confidence: number; detail?: string }[] | null }) {62 if (!provenance?.length) return <span className="text-dim">—</span>;63 const best = new Map<string, { confidence: number; detail?: string }>();64 for (const p of provenance) if (!best.has(p.surface) || best.get(p.surface)!.confidence < p.confidence) best.set(p.surface, p);65 const colors: Record<string, string> = { network: "#38e1ff", dom: "#43e69a", visual: "#ff7ad9", media: "#9d7bff", accessibility: "#ffb347", navigation: "#6b7a8c" };66 return (67 <span className="inline-flex gap-1 flex-wrap">68 {[...best.entries()].map(([s, p]) => (69 <Tag key={s} color={colors[s] ?? "#6b7a8c"} title={p.detail}>70 {s} {Math.round(p.confidence * 100)}71 </Tag>72 ))}73 </span>74 );75}7677export function Bar({ value, color = "#38e1ff", className }: { value: number; color?: string; className?: string }) {78 return (79 <div className={clsx("h-1.5 rounded-full bg-white/5 overflow-hidden", className)}>80 <div className="h-full rounded-full transition-all" style={{ width: `${Math.max(0, Math.min(100, value * 100))}%`, background: color }} />81 </div>82 );83}8485export function Empty({ children }: { children: ReactNode }) {86 return <div className="text-sm text-dim py-10 text-center">{children}</div>;87}8889export function Skeleton({ rows = 4 }: { rows?: number }) {90 return (91 <div className="space-y-2">92 {Array.from({ length: rows }).map((_, i) => (93 <div key={i} className="skeleton h-4" style={{ width: `${70 + ((i * 13) % 30)}%` }} />94 ))}95 </div>96 );97}9899export function Table({ head, children, dense }: { head: ReactNode[]; children: ReactNode; dense?: boolean }) {100 return (101 <div className="overflow-auto scrollbar-thin -mx-4 px-4">102 <table className={clsx("w-full text-sm", dense ? "text-[12.5px]" : "")}>103 <thead>104 <tr className="text-left text-[10.5px] uppercase tracking-widest text-dim">105 {head.map((h, i) => (106 <th key={i} className="px-2 py-2 font-medium border-b border-line whitespace-nowrap">107 {h}108 </th>109 ))}110 </tr>111 </thead>112 <tbody className="[&>tr]:border-b [&>tr]:border-line/60 [&>tr:hover]:bg-white/[0.025] [&>tr>td]:px-2 [&>tr>td]:py-1.5 [&>tr>td]:align-top">{children}</tbody>113 </table>114 </div>115 );116}117118export function Metric({ label, value }: { label: string; value: ReactNode }) {119 return (120 <div className="flex items-baseline justify-between gap-3 text-sm border-b border-line/60 py-1.5">121 <span className="text-dim">{label}</span>122 <span className="mono text-fg">{value}</span>123 </div>124 );125}126127export function StatusPill({ status }: { status: string }) {128 const map: Record<string, string> = { running: "#ffb347", healthy: "#ffb347", queued: "#6b7a8c", done: "#43e69a", stopped: "#6b7a8c", failed: "#ff5c7a", auth_required: "#ff5c7a", crashed: "#ff5c7a", degraded: "#ff5c7a" };129 return (130 <Tag color={map[status] ?? "#6b7a8c"}>131 {status === "running" || status === "healthy" ? <span className="relative inline-block h-1.5 w-1.5 rounded-full bg-amber text-amber live-dot mr-1" /> : null}132 {status}133 </Tag>134 );135}136