import Link from "next/link"; import type { ReactNode } from "react"; import { fmtScore, importanceBand, stateLabel, typeLabel } from "@/lib/format"; export function Score({ value, size = "md", title, kind = "importance" }: { value: number | null | undefined; size?: "sm" | "md" | "lg"; title?: string; kind?: "importance" | "signal" }) { const v = value ?? 0; const band = importanceBand(v); 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"; 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"; return ( {fmtScore(v)} ); } export type Tone = "default" | "silent" | "signal" | "danger" | "warn" | "info" | "ok" | "hot" | "high"; const TONES: Record = { default: "border-line bg-panel-2 text-fg-muted", silent: "border-silent/40 bg-silent-soft text-silent", signal: "border-signal/40 bg-signal-soft text-signal", danger: "border-danger/40 bg-danger/10 text-danger", warn: "border-warn/40 bg-warn/10 text-warn", info: "border-info/40 bg-info/10 text-info", ok: "border-ok/40 bg-ok/10 text-ok", hot: "border-hot/50 bg-hot/12 text-hot", high: "border-high/50 bg-high/12 text-high", }; export function Chip({ children, tone = "default", href, className = "", title }: { children: ReactNode; tone?: Tone; href?: string; className?: string; title?: string }) { 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}`; if (href) return {children}; return {children}; } /** Signal badges (spec §31): BREAKING · SILENT · FIRST PARTY · CONFIRMED · DEVELOPING · ANOMALOUS · EXTERNAL */ export type BadgeKind = "breaking" | "silent" | "first-party" | "confirmed" | "developing" | "anomalous" | "external" | "inferred" | "unconfirmed" | "replayed"; export function Badge({ kind, compact = false }: { kind: BadgeKind; compact?: boolean }) { const map: Record = { breaking: { tone: "hot", label: "BREAKING", short: "BRK", title: "Breaking: strong signal, fresh, confirmed or first-party" }, developing: { tone: "high", label: "DEVELOPING", short: "DEV", title: "Developing: signals accumulating across sources" }, silent: { tone: "silent", label: "SILENT", short: "SIL", title: "Silent change: modified without a matching announcement" }, "first-party": { tone: "signal", label: "FIRST PARTY", short: "1ST", title: "First-party evidence: the organization's own channel" }, confirmed: { tone: "ok", label: "CONFIRMED", short: "CFM", title: "Confirmed by independent sources" }, anomalous: { tone: "warn", label: "ANOMALOUS", short: "ANM", title: "Activity far above this source's baseline" }, external: { tone: "default", label: "EXTERNAL", short: "EXT", title: "Third-party report (media / aggregator)" }, inferred: { tone: "info", label: "INFERRED", short: "INF", title: "Classification inferred, limited evidence" }, unconfirmed: { tone: "warn", label: "UNCONFIRMED", short: "UNC", title: "Single-source, low heuristic confidence" }, replayed: { tone: "default", label: "REPLAYED", short: "RPL", title: "Delivered from the durable stream after a reconnection" }, }; const b = map[kind]; return ( {compact ? b.short : b.label} ); } export function StateBadge({ state }: { state: string | null | undefined }) { if (!state || state === "watching" || state === "closed") return null; return ; } export function TypeChip({ type, href }: { type: string; href?: string }) { return {typeLabel(type)}; } export function SilentBadge({ compact = false }: { compact?: boolean }) { return ; } export function EvidenceTag({ label }: { label: string | null | undefined }) { const l = (label ?? "OBSERVED").toUpperCase(); if (l === "CONFIRMED") return ; if (l === "INFERRED") return ; if (l === "UNCONFIRMED") return ; return {l}; } export function HealthPill({ health }: { health: string | null | undefined }) { const h = (health ?? "UP").toUpperCase(); 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"; return ( {h} ); } export function TierBadge({ tier }: { tier: string | null | undefined }) { return {tier ?? "?"}; } export function Panel({ title, action, children, className = "", dense = false, id }: { title?: ReactNode; action?: ReactNode; children: ReactNode; className?: string; dense?: boolean; id?: string }) { return ( {title !== undefined && ( {title} {action} )} {children} ); } export function Empty({ children = "No data yet — the engine is warming up.", icon }: { children?: ReactNode; icon?: ReactNode }) { return ( {icon} {children} ); } export function Stat({ label, value, hint, tone }: { label: string; value: ReactNode; hint?: ReactNode; tone?: Tone }) { const color = tone === "hot" ? "text-hot" : tone === "silent" ? "text-silent" : tone === "signal" ? "text-signal" : tone === "warn" ? "text-warn" : ""; return ( {label} {value} {hint && {hint}} ); } export function Bar({ value, max = 100, tone = "signal" }: { value: number; max?: number; tone?: "signal" | "hot" | "high" | "mid" | "silent" | "info" }) { const pct = Math.max(0, Math.min(100, (value / max) * 100)); const c = tone === "hot" ? "bg-hot" : tone === "high" ? "bg-high" : tone === "mid" ? "bg-mid" : tone === "silent" ? "bg-silent" : tone === "info" ? "bg-info" : "bg-signal"; return ( ); } export function Gauge({ label, value, tone }: { label: string; value: number | null | undefined; tone?: "signal" | "hot" | "high" | "mid" | "silent" | "info" }) { const v = value ?? 0; const band = importanceBand(v); const t = tone ?? (band === "hot" ? "hot" : band === "high" ? "high" : band === "mid" ? "mid" : "signal"); return ( {label} {fmtScore(v)} ); } export function PageHeader({ title, kicker, description, actions, compact = false }: { title: ReactNode; kicker?: ReactNode; description?: ReactNode; actions?: ReactNode; compact?: boolean }) { return ( {kicker && {kicker}} {title} {description && {description}} {actions && {actions}} ); } export function Mono({ children, className = "" }: { children: ReactNode; className?: string }) { return {children}; } export function Table({ head, children, className = "" }: { head: ReactNode[]; children: ReactNode; className?: string }) { return ( {head.map((h, i) => ( {h} ))} {children} ); } export function Td({ children, className = "", mono = false, colSpan }: { children?: ReactNode; className?: string; mono?: boolean; colSpan?: number }) { return ( {children} ); } export function ExtLink({ href, children, className = "" }: { href: string; children: ReactNode; className?: string }) { return ( {children} ); } export function Kbd({ children }: { children: ReactNode }) { return {children}; } /** Skeleton primitives (spec §113). */ export function Skeleton({ className = "" }: { className?: string }) { return ; } export function SkeletonRows({ rows = 8 }: { rows?: number }) { return ( {Array.from({ length: rows }, (_, i) => ( ))} ); } export function SkeletonPanel({ lines = 5, title = true }: { lines?: number; title?: boolean }) { return ( {title && } {Array.from({ length: lines }, (_, i) => ( ))} ); } /** Inline SVG sparkline — no chart library, no client JS. */ export 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 }) { if (!values.length) return ; const max = Math.max(1, ...values); const step = values.length > 1 ? width / (values.length - 1) : width; const pts = values.map((v, i) => [i * step, height - 2 - (v / max) * (height - 4)] as const); const d = pts.map(([x, y], i) => `${i ? "L" : "M"}${x.toFixed(1)},${y.toFixed(1)}`).join(" "); const color = tone === "hot" ? "var(--hot)" : tone === "info" ? "var(--info)" : tone === "silent" ? "var(--silent)" : tone === "muted" ? "var(--fg-subtle)" : "var(--signal)"; return ( {fill && } ); } /** 35-day activity heatmap (spec §102). */ export function Heatmap({ days, max }: { days: { day: string; events: number; silent?: number; breaking?: number }[]; max?: number }) { const m = max ?? Math.max(1, ...days.map((d) => d.events)); const cls = (n: number, breaking?: number): string => { if (!n) return "heat-0"; if (breaking && breaking >= 3) return "heat-hot"; const r = n / m; return r > 0.75 ? "heat-4" : r > 0.5 ? "heat-3" : r > 0.25 ? "heat-2" : "heat-1"; }; return ( {days.map((d) => ( ))} ); } /** Link-based tabs (URL-driven, server-renderable). */ export function Tabs({ items, current, className = "" }: { items: { key: string; label: ReactNode; href: string; count?: number | null }[]; current: string; className?: string }) { return ( {items.map((t) => { const active = t.key === current; return ( {t.label} {t.count !== undefined && t.count !== null && {t.count}} ); })} ); } export function StateLabelText({ state }: { state?: string | null }) { const l = stateLabel(state); if (!l) return null; const c = state === "breaking" ? "text-hot" : state === "developing" ? "text-high" : state === "confirmed" ? "text-ok" : "text-fg-subtle"; return {l}; } export function Flag({ code, className = "" }: { code?: string | null; className?: string }) { if (!code) return null; const c = code.toUpperCase(); if (c.length !== 2) return {c}; const flag = String.fromCodePoint(...[...c].map((ch) => 0x1f1e6 + ch.charCodeAt(0) - 65)); return {flag}; } /** Entity mark (spec §111): deterministic initials fallback — no third-party favicon requests. */ export function Mark({ name, size = 5, className = "" }: { name: string; size?: 4 | 5 | 6 | 8; className?: string }) { const words = name.replace(/[^\p{L}\p{N} ]/gu, " ").trim().split(/\s+/).filter(Boolean); const initials = (words.length >= 2 ? words[0]![0]! + words[1]![0]! : name.slice(0, 2)).toUpperCase(); let h = 0; for (const ch of name) h = (h * 31 + ch.charCodeAt(0)) >>> 0; const hue = h % 360; const sz = size === 4 ? "size-4 text-[8px]" : size === 6 ? "size-6 text-[10px]" : size === 8 ? "size-8 text-[12px]" : "size-5 text-[9px]"; return ( {initials} ); }
{description}