SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
4 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
3.3 KB · 84 lines tsx
Raw Blame History
1"use client";23import type { SourceKind } from "@dci/core";4import { Archive, ExternalLink } from "lucide-react";5import { fmtDate } from "@/lib/format";6import { SOURCE_KIND_LABEL } from "@/lib/labels";7import { Badge } from "./badge";8import { cn } from "./cn";9import { Tooltip } from "./popover";1011/**12 * Provenance / source-kind badge (OPERATOR · GOVERNMENT · UTILITY · REGULATORY · FILING · DATASET · NEWS · COMMUNITY…).13 * Primary kinds (operator, government, utility, filing, cloud provider, registry) get a filled dot; secondary kinds14 * (news, secondary, dataset, community) a hollow one. Hover / focus shows the source, published & retrieved dates and15 * whether an archived copy exists (documentId).16 */17export interface SourceBadgeProps {18  kind: SourceKind;19  name?: string | null;20  url?: string | null;21  publishedAt?: string | null;22  retrievedAt?: string | null;23  /** an archived document id means the page was captured and can be reprocessed */24  documentId?: string | null;25  size?: "xs" | "sm";26  /** show the source name next to the kind */27  withName?: boolean;28  className?: string;29}3031const PRIMARY: ReadonlySet<SourceKind> = new Set<SourceKind>(["operator", "government", "utility", "filing", "cloud_provider", "registry"]);3233/** Short upper-case kind label used in dense tables ("REGULATORY" for government filings when the source says so). */34export const SOURCE_KIND_CAPS: Record<SourceKind, string> = {35  operator: "OPERATOR",36  government: "GOVERNMENT",37  filing: "FILING",38  utility: "UTILITY",39  dataset: "DATASET",40  community: "COMMUNITY",41  secondary: "SECONDARY",42  news: "NEWS",43  cloud_provider: "CLOUD",44  registry: "REGISTRY",45};4647export function SourceBadge({ kind, name, url, publishedAt, retrievedAt, documentId, size = "xs", withName, className }: SourceBadgeProps) {48  const primary = PRIMARY.has(kind);49  const inner = (50    <Badge tone="outline" size={size} className={cn("cursor-help", className)}>51      <span aria-hidden className={cn("inline-block size-[6px] rounded-full", primary ? "bg-ink-2" : "border-[1.5px] border-ink-3")} />52      <span className="tracking-[0.04em]">{SOURCE_KIND_CAPS[kind]}</span>53      {withName && name && <span className="max-w-[140px] truncate font-normal normal-case tracking-normal text-ink-3">{name}</span>}54    </Badge>55  );56  const details = (57    <div className="flex flex-col gap-0.5">58      <div className="font-medium">{name ?? SOURCE_KIND_LABEL[kind]}</div>59      <div className="opacity-80">60        {SOURCE_KIND_LABEL[kind]} · {primary ? "primary source" : "secondary source"}61      </div>62      {publishedAt && <div className="opacity-80">Published {fmtDate(publishedAt)}</div>}63      {retrievedAt && <div className="opacity-80">Retrieved {fmtDate(retrievedAt)}</div>}64      <div className="inline-flex items-center gap-1 opacity-80">65        <Archive className="size-3" aria-hidden /> {documentId ? "Archived copy on file" : "No archived copy recorded"}66      </div>67      {url && (68        <div className="inline-flex items-center gap-1 truncate opacity-80">69          <ExternalLink className="size-3" aria-hidden /> {safeHost(url)}70        </div>71      )}72    </div>73  );74  return <Tooltip content={details}>{inner}</Tooltip>;75}7677function safeHost(url: string): string {78  try {79    return new URL(url).host.replace(/^www\./, "");80  } catch {81    return url;82  }83}84