TypeScript 55.4%
Python 43.2%
SQL 1.2%
1import Link from "next/link";2import type { ReactNode } from "react";3import { fmtScore, importanceBand, stateLabel, typeLabel } from "@/lib/format";45export function Score({ value, size = "md", title, kind = "importance" }: { value: number | null | undefined; size?: "sm" | "md" | "lg"; title?: string; kind?: "importance" | "signal" }) {6 const v = value ?? 0;7 const band = importanceBand(v);8 const color = band === "hot" ? "bg-hot/15 text-hot border-hot/40" : band === "high" ? "bg-high/15 text-high border-high/40" : band === "mid" ? "bg-mid/15 text-mid border-mid/40" : "bg-panel-2 text-fg-muted border-line";9 const sz = size === "sm" ? "min-w-7 px-1 text-[11px] h-5" : size === "lg" ? "min-w-14 px-2 text-xl h-9" : "min-w-9 px-1.5 text-xs h-6";10 return (11 <span title={title ?? `${kind === "signal" ? "Signal" : "Importance"} ${fmtScore(v)}`} className={`inline-flex items-center justify-center rounded-sm border font-mono font-semibold tabular ${color} ${sz}`}>12 {fmtScore(v)}13 </span>14 );15}1617export type Tone = "default" | "silent" | "signal" | "danger" | "warn" | "info" | "ok" | "hot" | "high";1819const TONES: Record<Tone, string> = {20 default: "border-line bg-panel-2 text-fg-muted",21 silent: "border-silent/40 bg-silent-soft text-silent",22 signal: "border-signal/40 bg-signal-soft text-signal",23 danger: "border-danger/40 bg-danger/10 text-danger",24 warn: "border-warn/40 bg-warn/10 text-warn",25 info: "border-info/40 bg-info/10 text-info",26 ok: "border-ok/40 bg-ok/10 text-ok",27 hot: "border-hot/50 bg-hot/12 text-hot",28 high: "border-high/50 bg-high/12 text-high",29};3031export function Chip({ children, tone = "default", href, className = "", title }: { children: ReactNode; tone?: Tone; href?: string; className?: string; title?: string }) {32 const cls = `inline-flex items-center gap-1 rounded-sm border px-1.5 py-px text-[10.5px] font-medium leading-4 whitespace-nowrap ${TONES[tone]} ${className}`;33 if (href) return <Link href={href} title={title} className={`${cls} hover:border-line-strong`}>{children}</Link>;34 return <span title={title} className={cls}>{children}</span>;35}3637/** Signal badges (spec §31): BREAKING · SILENT · FIRST PARTY · CONFIRMED · DEVELOPING · ANOMALOUS · EXTERNAL */38export type BadgeKind = "breaking" | "silent" | "first-party" | "confirmed" | "developing" | "anomalous" | "external" | "inferred" | "unconfirmed" | "replayed";39export function Badge({ kind, compact = false }: { kind: BadgeKind; compact?: boolean }) {40 const map: Record<BadgeKind, { tone: Tone; label: string; short: string; title: string }> = {41 breaking: { tone: "hot", label: "BREAKING", short: "BRK", title: "Breaking: strong signal, fresh, confirmed or first-party" },42 developing: { tone: "high", label: "DEVELOPING", short: "DEV", title: "Developing: signals accumulating across sources" },43 silent: { tone: "silent", label: "SILENT", short: "SIL", title: "Silent change: modified without a matching announcement" },44 "first-party": { tone: "signal", label: "FIRST PARTY", short: "1ST", title: "First-party evidence: the organization's own channel" },45 confirmed: { tone: "ok", label: "CONFIRMED", short: "CFM", title: "Confirmed by independent sources" },46 anomalous: { tone: "warn", label: "ANOMALOUS", short: "ANM", title: "Activity far above this source's baseline" },47 external: { tone: "default", label: "EXTERNAL", short: "EXT", title: "Third-party report (media / aggregator)" },48 inferred: { tone: "info", label: "INFERRED", short: "INF", title: "Classification inferred, limited evidence" },49 unconfirmed: { tone: "warn", label: "UNCONFIRMED", short: "UNC", title: "Single-source, low heuristic confidence" },50 replayed: { tone: "default", label: "REPLAYED", short: "RPL", title: "Delivered from the durable stream after a reconnection" },51 };52 const b = map[kind];53 return (54 <Chip tone={b.tone} title={b.title} className="font-mono font-semibold tracking-wider">55 {compact ? b.short : b.label}56 </Chip>57 );58}5960export function StateBadge({ state }: { state: string | null | undefined }) {61 if (!state || state === "watching" || state === "closed") return null;62 return <Badge kind={state as BadgeKind} />;63}6465export function TypeChip({ type, href }: { type: string; href?: string }) {66 return <Chip href={href}>{typeLabel(type)}</Chip>;67}6869export function SilentBadge({ compact = false }: { compact?: boolean }) {70 return <Badge kind="silent" compact={compact} />;71}7273export function EvidenceTag({ label }: { label: string | null | undefined }) {74 const l = (label ?? "OBSERVED").toUpperCase();75 if (l === "CONFIRMED") return <Badge kind="confirmed" />;76 if (l === "INFERRED") return <Badge kind="inferred" />;77 if (l === "UNCONFIRMED") return <Badge kind="unconfirmed" />;78 return <Chip className="font-mono tracking-wider">{l}</Chip>;79}8081export function HealthPill({ health }: { health: string | null | undefined }) {82 const h = (health ?? "UP").toUpperCase();83 const tone: Tone = h === "UP" || h === "ACTIVE" || h === "VALIDATED" ? "ok" : h === "DEGRADED" || h === "RATE_LIMITED" || h === "PENDING" ? "warn" : h === "ERROR" ? "danger" : h === "SHADOW" ? "info" : "default";84 return (85 <Chip tone={tone} className="font-mono">86 <span className={`inline-block size-1.5 rounded-full ${tone === "ok" ? "bg-ok" : tone === "danger" ? "bg-danger" : tone === "default" ? "bg-low" : tone === "info" ? "bg-info" : "bg-warn"}`} /> {h}87 </Chip>88 );89}9091export function TierBadge({ tier }: { tier: string | null | undefined }) {92 return <span className="inline-flex size-5 items-center justify-center rounded-sm border border-line bg-panel-2 font-mono text-[11px] font-semibold text-fg-muted" title={`Tier ${tier}`}>{tier ?? "?"}</span>;93}9495export function Panel({ title, action, children, className = "", dense = false, id }: { title?: ReactNode; action?: ReactNode; children: ReactNode; className?: string; dense?: boolean; id?: string }) {96 return (97 <section id={id} className={`panel ${className}`}>98 {title !== undefined && (99 <header className="flex items-center justify-between gap-3 border-b border-line px-3 py-2">100 <h2 className="label">{title}</h2>101 {action}102 </header>103 )}104 <div className={dense ? "" : "p-3"}>{children}</div>105 </section>106 );107}108109export function Empty({ children = "No data yet — the engine is warming up.", icon }: { children?: ReactNode; icon?: ReactNode }) {110 return (111 <div className="flex flex-col items-center gap-1 px-3 py-8 text-center text-[13px] text-fg-subtle">112 {icon}113 <div>{children}</div>114 </div>115 );116}117118export function Stat({ label, value, hint, tone }: { label: string; value: ReactNode; hint?: ReactNode; tone?: Tone }) {119 const color = tone === "hot" ? "text-hot" : tone === "silent" ? "text-silent" : tone === "signal" ? "text-signal" : tone === "warn" ? "text-warn" : "";120 return (121 <div className="flex min-w-0 flex-col gap-0.5 px-3 py-2">122 <span className="label">{label}</span>123 <span className={`font-mono text-lg font-semibold leading-tight tabular ${color}`}>{value}</span>124 {hint && <span className="truncate text-[11px] text-fg-subtle">{hint}</span>}125 </div>126 );127}128129export function Bar({ value, max = 100, tone = "signal" }: { value: number; max?: number; tone?: "signal" | "hot" | "high" | "mid" | "silent" | "info" }) {130 const pct = Math.max(0, Math.min(100, (value / max) * 100));131 const c = tone === "hot" ? "bg-hot" : tone === "high" ? "bg-high" : tone === "mid" ? "bg-mid" : tone === "silent" ? "bg-silent" : tone === "info" ? "bg-info" : "bg-signal";132 return (133 <div className="h-1 w-full overflow-hidden rounded-full bg-panel-2">134 <div className={`h-full rounded-full ${c}`} style={{ width: `${pct}%` }} />135 </div>136 );137}138139export function Gauge({ label, value, tone }: { label: string; value: number | null | undefined; tone?: "signal" | "hot" | "high" | "mid" | "silent" | "info" }) {140 const v = value ?? 0;141 const band = importanceBand(v);142 const t = tone ?? (band === "hot" ? "hot" : band === "high" ? "high" : band === "mid" ? "mid" : "signal");143 return (144 <div className="flex flex-col gap-1">145 <div className="flex items-baseline justify-between">146 <span className="label">{label}</span>147 <span className="font-mono text-base font-semibold tabular">{fmtScore(v)}</span>148 </div>149 <Bar value={v} tone={t} />150 </div>151 );152}153154export function PageHeader({ title, kicker, description, actions, compact = false }: { title: ReactNode; kicker?: ReactNode; description?: ReactNode; actions?: ReactNode; compact?: boolean }) {155 return (156 <div className={`${compact ? "mb-3" : "mb-4"} flex flex-wrap items-end justify-between gap-3`}>157 <div className="min-w-0">158 {kicker && <div className="label mb-1 flex flex-wrap items-center gap-2 normal-case tracking-normal">{kicker}</div>}159 <h1 className={`${compact ? "text-lg" : "text-xl sm:text-2xl"} font-semibold leading-tight`}>{title}</h1>160 {description && <p className="mt-1 max-w-3xl text-[13px] text-fg-muted">{description}</p>}161 </div>162 {actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}163 </div>164 );165}166167export function Mono({ children, className = "" }: { children: ReactNode; className?: string }) {168 return <span className={`font-mono tabular ${className}`}>{children}</span>;169}170171export function Table({ head, children, className = "" }: { head: ReactNode[]; children: ReactNode; className?: string }) {172 return (173 <div className={`overflow-x-auto ${className}`}>174 <table className="w-full text-[12.5px]">175 <thead>176 <tr className="border-b border-line text-left">177 {head.map((h, i) => (178 <th key={i} className="label whitespace-nowrap px-3 py-2 font-semibold">179 {h}180 </th>181 ))}182 </tr>183 </thead>184 <tbody className="divide-y divide-line">{children}</tbody>185 </table>186 </div>187 );188}189190export function Td({ children, className = "", mono = false, colSpan }: { children?: ReactNode; className?: string; mono?: boolean; colSpan?: number }) {191 return (192 <td colSpan={colSpan} className={`px-3 py-1.5 align-top ${mono ? "font-mono tabular text-[12px]" : ""} ${className}`}>193 {children}194 </td>195 );196}197198export function ExtLink({ href, children, className = "" }: { href: string; children: ReactNode; className?: string }) {199 return (200 <a href={href} target="_blank" rel="noopener noreferrer nofollow" className={`text-info hover:underline ${className}`}>201 {children}202 </a>203 );204}205206export function Kbd({ children }: { children: ReactNode }) {207 return <kbd className="inline-flex h-[18px] min-w-[18px] items-center justify-center rounded border border-line bg-panel-2 px-1 font-mono text-[10px] text-fg-subtle">{children}</kbd>;208}209210/** Skeleton primitives (spec §113). */211export function Skeleton({ className = "" }: { className?: string }) {212 return <div aria-hidden className={`skeleton ${className}`} />;213}214export function SkeletonRows({ rows = 8 }: { rows?: number }) {215 return (216 <div className="divide-y divide-line">217 {Array.from({ length: rows }, (_, i) => (218 <div key={i} className="grid grid-cols-[6.5rem_1fr_auto] gap-x-3 px-3 py-2.5">219 <div className="flex flex-col gap-1.5">220 <Skeleton className="h-3 w-14" />221 <Skeleton className="h-2.5 w-10" />222 </div>223 <div className="flex flex-col gap-1.5">224 <Skeleton className="h-2.5 w-24" />225 <Skeleton className={`h-3.5 ${i % 3 === 0 ? "w-3/4" : i % 3 === 1 ? "w-11/12" : "w-2/3"}`} />226 <Skeleton className="h-2.5 w-40" />227 </div>228 <Skeleton className="h-6 w-9" />229 </div>230 ))}231 </div>232 );233}234export function SkeletonPanel({ lines = 5, title = true }: { lines?: number; title?: boolean }) {235 return (236 <div className="panel p-3">237 {title && <Skeleton className="mb-3 h-2.5 w-28" />}238 <div className="flex flex-col gap-2">239 {Array.from({ length: lines }, (_, i) => (240 <Skeleton key={i} className={`h-3 ${i % 2 ? "w-5/6" : "w-full"}`} />241 ))}242 </div>243 </div>244 );245}246247/** Inline SVG sparkline — no chart library, no client JS. */248export function Sparkline({ values, width = 120, height = 28, tone = "signal", fill = true, responsive = false }: { values: number[]; width?: number; height?: number; tone?: "signal" | "hot" | "info" | "silent" | "muted"; fill?: boolean; /** stretch to the container width (viewBox scaling) */ responsive?: boolean }) {249 if (!values.length) return <svg width={responsive ? "100%" : width} height={height} aria-hidden />;250 const max = Math.max(1, ...values);251 const step = values.length > 1 ? width / (values.length - 1) : width;252 const pts = values.map((v, i) => [i * step, height - 2 - (v / max) * (height - 4)] as const);253 const d = pts.map(([x, y], i) => `${i ? "L" : "M"}${x.toFixed(1)},${y.toFixed(1)}`).join(" ");254 const color = tone === "hot" ? "var(--hot)" : tone === "info" ? "var(--info)" : tone === "silent" ? "var(--silent)" : tone === "muted" ? "var(--fg-subtle)" : "var(--signal)";255 return (256 <svg width={responsive ? "100%" : width} height={height} viewBox={`0 0 ${width} ${height}`} preserveAspectRatio={responsive ? "none" : undefined} aria-hidden className="block max-w-full overflow-visible">257 {fill && <path d={`${d} L${width},${height} L0,${height} Z`} fill={color} opacity={0.12} />}258 <path d={d} fill="none" stroke={color} strokeWidth={1.5} strokeLinejoin="round" strokeLinecap="round" />259 </svg>260 );261}262263/** 35-day activity heatmap (spec §102). */264export function Heatmap({ days, max }: { days: { day: string; events: number; silent?: number; breaking?: number }[]; max?: number }) {265 const m = max ?? Math.max(1, ...days.map((d) => d.events));266 const cls = (n: number, breaking?: number): string => {267 if (!n) return "heat-0";268 if (breaking && breaking >= 3) return "heat-hot";269 const r = n / m;270 return r > 0.75 ? "heat-4" : r > 0.5 ? "heat-3" : r > 0.25 ? "heat-2" : "heat-1";271 };272 return (273 <div className="flex flex-wrap gap-[3px]" role="img" aria-label="Daily activity over the last 35 days">274 {days.map((d) => (275 <span key={d.day} title={`${d.day}: ${d.events} event${d.events === 1 ? "" : "s"}${d.silent ? ` · ${d.silent} silent` : ""}${d.breaking ? ` · ${d.breaking} breaking` : ""}`} className={`size-3 rounded-[2px] ${cls(d.events, d.breaking)}`} />276 ))}277 </div>278 );279}280281/** Link-based tabs (URL-driven, server-renderable). */282export function Tabs({ items, current, className = "" }: { items: { key: string; label: ReactNode; href: string; count?: number | null }[]; current: string; className?: string }) {283 return (284 <nav className={`flex gap-0.5 overflow-x-auto border-b border-line no-scrollbar ${className}`} aria-label="Sections">285 {items.map((t) => {286 const active = t.key === current;287 return (288 <Link key={t.key} href={t.href} aria-current={active ? "page" : undefined} className={`-mb-px inline-flex items-center gap-1.5 whitespace-nowrap border-b-2 px-2.5 py-1.5 text-[12.5px] ${active ? "border-signal text-fg" : "border-transparent text-fg-muted hover:text-fg"}`}>289 {t.label}290 {t.count !== undefined && t.count !== null && <span className="font-mono text-[10.5px] text-fg-subtle tabular">{t.count}</span>}291 </Link>292 );293 })}294 </nav>295 );296}297298export function StateLabelText({ state }: { state?: string | null }) {299 const l = stateLabel(state);300 if (!l) return null;301 const c = state === "breaking" ? "text-hot" : state === "developing" ? "text-high" : state === "confirmed" ? "text-ok" : "text-fg-subtle";302 return <span className={`font-mono text-[10.5px] font-semibold tracking-wider ${c}`}>{l}</span>;303}304305export function Flag({ code, className = "" }: { code?: string | null; className?: string }) {306 if (!code) return null;307 const c = code.toUpperCase();308 if (c.length !== 2) return <span className={`font-mono text-[10px] ${className}`}>{c}</span>;309 const flag = String.fromCodePoint(...[...c].map((ch) => 0x1f1e6 + ch.charCodeAt(0) - 65));310 return <span className={className} title={c} aria-label={c}>{flag}</span>;311}312313/** Entity mark (spec §111): deterministic initials fallback — no third-party favicon requests. */314export function Mark({ name, size = 5, className = "" }: { name: string; size?: 4 | 5 | 6 | 8; className?: string }) {315 const words = name.replace(/[^\p{L}\p{N} ]/gu, " ").trim().split(/\s+/).filter(Boolean);316 const initials = (words.length >= 2 ? words[0]![0]! + words[1]![0]! : name.slice(0, 2)).toUpperCase();317 let h = 0;318 for (const ch of name) h = (h * 31 + ch.charCodeAt(0)) >>> 0;319 const hue = h % 360;320 const sz = size === 4 ? "size-4 text-[8px]" : size === 6 ? "size-6 text-[10px]" : size === 8 ? "size-8 text-[12px]" : "size-5 text-[9px]";321 return (322 <span aria-hidden className={`inline-flex shrink-0 items-center justify-center rounded-[3px] border font-mono font-semibold tracking-tight ${sz} ${className}`} style={{ background: `color-mix(in oklab, oklch(0.62 0.12 ${hue}) 22%, var(--panel-2))`, borderColor: `color-mix(in oklab, oklch(0.62 0.12 ${hue}) 40%, var(--line))`, color: `oklch(0.78 0.11 ${hue})` }}>323 {initials}324 </span>325 );326}327