spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { SourceHistoryItem } from "@dci/core";2import { ExternalLink } from "lucide-react";3import type { ReactNode } from "react";4import { fmtDate, fmtValue } from "@/lib/format";5import { SOURCE_KIND_LABEL } from "@/lib/labels";6import { cn } from "./cn";78export interface TimelineEntry {9 date: string;10 title: ReactNode;11 description?: ReactNode;12 meta?: ReactNode;13 href?: string | null;14 tone?: "neutral" | "accent" | "ok" | "warn" | "danger";15}1617const toneDot: Record<NonNullable<TimelineEntry["tone"]>, string> = {18 neutral: "bg-ink-4",19 accent: "bg-accent",20 ok: "bg-ok",21 warn: "bg-warn",22 danger: "bg-danger",23};2425/** Vertical timeline with hairline rail; dates in mono. */26export function TimelineList({ items, className, dense }: { items: TimelineEntry[]; className?: string; dense?: boolean }) {27 return (28 <ol className={cn("relative ml-1.5 border-l border-hair", className)}>29 {items.map((it, i) => (30 <li key={i} className={cn("relative pl-4", dense ? "pb-2.5" : "pb-4", "last:pb-0")}>31 <span aria-hidden className={cn("absolute -left-[4.5px] top-[6px] size-2 rounded-full ring-2 ring-surface", toneDot[it.tone ?? "neutral"])} />32 <div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">33 <time className="figure text-[11.5px] text-ink-3" dateTime={it.date}>34 {fmtDate(it.date)}35 </time>36 <span className="min-w-0 break-words text-[13px] font-medium text-ink">{it.title}</span>37 </div>38 {it.description && <div className="mt-0.5 text-[12.5px] text-ink-2">{it.description}</div>}39 {(it.meta || it.href) && (40 <div className="mt-0.5 flex flex-wrap items-center gap-x-2 text-[11.5px] text-ink-3">41 {it.meta}42 {it.href && (43 <a href={it.href} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 hover:text-accent">44 source <ExternalLink className="size-3" />45 </a>46 )}47 </div>48 )}49 </li>50 ))}51 </ol>52 );53}5455/** Adapter for SourceHistoryItem[] (entity pages). */56export function SourceHistory({ items, className }: { items: SourceHistoryItem[]; className?: string }) {57 const entries: TimelineEntry[] = items.map((s) => ({58 date: s.date,59 title: prettyDescription(s.description),60 meta: (61 <span>62 {s.sourceName} · {SOURCE_KIND_LABEL[s.sourceKind]} · {s.kind}63 </span>64 ),65 href: s.url,66 tone: s.kind === "changed" ? "accent" : s.kind === "verified" ? "ok" : "neutral",67 }));68 return <TimelineList items={entries} className={className} dense />;69}7071/** "field observed: {json}" → "field observed: human value" (structured payloads are otherwise one unbreakable token). */72function prettyDescription(d: string): string {73 const m = /^(.*?:\s*)([[{].*[\]}])\s*$/s.exec(d);74 if (!m) return d;75 try {76 return `${m[1]}${fmtValue(JSON.parse(m[2]!))}`;77 } catch {78 return d;79 }80}81