SPB Git forge
7commits 1branches 0releases
229.0 KBsize
maindefault branch
12 days agolast push
TypeScript 91.8% HTML 3.2% JavaScript 3% SQL 1.4% CSS 0.7%
6.1 KB · 136 lines tsx
Raw Blame History
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