SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
yesterdaylast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
10.2 KB · 218 lines tsx
Raw Blame History
1import type { AiEvidence, AuthorityTier, ClaimScope, ConfidenceLevel, EventType, FacilityStatus, FacilityType, GeoPrecision, ProjectStage, Significance } from "@dci/core";2import type { ReactNode } from "react";3import { AI_EVIDENCE_LABEL, AUTHORITY_TIER_LABEL, CONFIDENCE_LABEL, CONFIDENCE_RANK, CONFIDENCE_SHORT, EVENT_LABEL, PRECISION_LABEL, PRECISION_SHORT, SCOPE_LABEL, SCOPE_SHORT, SIGNIFICANCE_LABEL, STAGE_LABEL, STATUS_LABEL, STATUS_SHORT, STATUS_TONE, TYPE_LABEL, eventTone, isImprecise, isSiteScopeLabel, semanticsLabel, significanceBand, statusVar } from "@/lib/labels";4import { cn } from "./cn";5import { StatusDot } from "./status-dot";67export type BadgeTone = "neutral" | "accent" | "ok" | "warn" | "danger" | "info" | "outline";89const tones: Record<BadgeTone, string> = {10  neutral: "border-hair bg-surface-2 text-ink-2",11  outline: "border-hair-2 bg-transparent text-ink-2",12  accent: "border-accent/30 bg-accent-soft text-accent-ink",13  ok: "border-ok/25 bg-ok/10 text-ok",14  warn: "border-warn/30 bg-warn/10 text-warn",15  danger: "border-danger/30 bg-danger/10 text-danger",16  info: "border-info/25 bg-info/10 text-info",17};1819export interface BadgeProps {20  tone?: BadgeTone;21  size?: "xs" | "sm";22  className?: string;23  children: ReactNode;24  title?: string;25  mono?: boolean;26}2728export function Badge({ tone = "neutral", size = "sm", className, children, title, mono }: BadgeProps) {29  return (30    <span31      title={title}32      className={cn(33        "inline-flex max-w-full items-center gap-1.5 whitespace-nowrap rounded-xs border font-medium leading-none",34        size === "xs" ? "h-[18px] px-1.5 text-[10.5px]" : "h-[22px] px-2 text-[11.5px]",35        mono && "figure",36        tones[tone],37        className,38      )}39    >40      {children}41    </span>42  );43}4445/** Facility status badge — dot colored by tone, exact label. */46export function StatusBadge({ status, short, size = "sm", className }: { status: FacilityStatus; short?: boolean; size?: "xs" | "sm"; className?: string }) {47  const tone = STATUS_TONE[status];48  return (49    <span50      className={cn(51        "inline-flex items-center gap-1.5 whitespace-nowrap rounded-xs border font-medium leading-none",52        size === "xs" ? "h-[18px] px-1.5 text-[10.5px]" : "h-[22px] px-2 text-[11.5px]",53        "border-hair bg-surface text-ink-2",54        className,55      )}56      style={{ borderColor: `color-mix(in oklab, ${statusVar(tone)} 40%, var(--hair))` }}57      title={STATUS_LABEL[status]}58    >59      <StatusDot status={status} size={7} />60      <span>{short ? STATUS_SHORT[status] : STATUS_LABEL[status]}</span>61    </span>62  );63}6465export function TypeBadge({ type, size = "sm", className }: { type: FacilityType; size?: "xs" | "sm"; className?: string }) {66  const emphasis = type === "ai" || type === "hyperscale";67  return (68    <Badge tone={emphasis ? "accent" : "outline"} size={size} className={className} title={`Facility type: ${TYPE_LABEL[type]}`}>69      {TYPE_LABEL[type]}70    </Badge>71  );72}7374/** Confidence meter: 4 dots + label. */75export function ConfidenceBadge({ level, short = true, size = "sm", className }: { level: ConfidenceLevel; short?: boolean; size?: "xs" | "sm"; className?: string }) {76  const rank = CONFIDENCE_RANK[level];77  return (78    <Badge tone="outline" size={size} className={className} title={CONFIDENCE_LABEL[level]}>79      <span className="inline-flex items-center gap-[2px]" aria-hidden>80        {[0, 1, 2, 3].map((i) => (81          <span key={i} className={cn("inline-block size-[5px] rounded-full", i < rank ? (level === "estimated" ? "bg-warn" : "bg-ink-2") : "bg-hair-2")} />82        ))}83      </span>84      <span>{short ? CONFIDENCE_SHORT[level] : CONFIDENCE_LABEL[level]}</span>85    </Badge>86  );87}8889/** Geo precision: filled square = exact/parcel/street, hollow = city/metro/approx. */90export function PrecisionBadge({ precision, short = true, size = "sm", className }: { precision: GeoPrecision; short?: boolean; size?: "xs" | "sm"; className?: string }) {91  const hollow = isImprecise(precision);92  return (93    <Badge tone="outline" size={size} className={className} title={PRECISION_LABEL[precision]}>94      <span aria-hidden className={cn("inline-block size-[7px] rounded-full", hollow ? "border-[1.5px] border-ink-3" : "bg-ink-2")} />95      <span>{short ? PRECISION_SHORT[precision] : PRECISION_LABEL[precision]}</span>96    </Badge>97  );98}99100export function EventBadge({ type, size = "sm", className }: { type: EventType; size?: "xs" | "sm"; className?: string }) {101  const tone = eventTone(type);102  const style = tone === "neutral" || tone === "accent" ? undefined : { borderColor: `color-mix(in oklab, ${statusVar(tone)} 45%, var(--hair))`, color: statusVar(tone) };103  return (104    <Badge tone={tone === "accent" ? "accent" : "outline"} size={size} className={className} title={EVENT_LABEL[type]}>105      <span style={style ? { color: style.color } : undefined}>{EVENT_LABEL[type]}</span>106    </Badge>107  );108}109110/** Small "AI" / "Hyperscale" flags for rows. */111export function FlagBadges({ isAi, isHyperscale, size = "xs" }: { isAi?: boolean; isHyperscale?: boolean; size?: "xs" | "sm" }) {112  if (!isAi && !isHyperscale) return null;113  return (114    <span className="inline-flex gap-1">115      {isAi && (116        <Badge tone="accent" size={size} title="AI infrastructure">117          AI118        </Badge>119      )}120      {isHyperscale && (121        <Badge tone="outline" size={size} title="Hyperscale">122          HS123        </Badge>124      )}125    </span>126  );127}128129/** Estimated-value marker (≈) with tooltip title. */130export function EstimateMark({ className }: { className?: string }) {131  return (132    <span className={cn("text-ink-3", className)} title="Estimated value" aria-label="estimated">133      ≈134    </span>135  );136}137138/* ------------------------------------------------------------------ claim-first visual language (2026-09-12) */139140/**141 * Scope of a figure — building / facility / campus (site scopes, filled square) vs metro / country / portfolio /142 * company / unknown (non-site, hollow dashed square). Non-site scopes never populate a record's own MW.143 */144export function ScopeBadge({ scope, size = "xs", short = true, className }: { scope: ClaimScope | string | null | undefined; size?: "xs" | "sm"; short?: boolean; className?: string }) {145  if (!scope) return null;146  const s = (scope in SCOPE_LABEL ? scope : "unknown") as ClaimScope;147  const site = isSiteScopeLabel(s);148  return (149    <Badge tone="outline" size={size} className={className} title={`${SCOPE_LABEL[s]} — ${site ? "site-scoped figure: describes this site" : "not site-scoped: stored as evidence, never summed into this record"}`}>150      <span aria-hidden className={cn("inline-block size-[7px] rounded-[1px]", site ? "bg-ink-2" : "border border-dashed border-ink-3")} />151      <span>{short ? SCOPE_SHORT[s] : SCOPE_LABEL[s]}</span>152    </Badge>153  );154}155156/** What a MW figure means (IT capacity / total power / planned / grid connection / utility / ultimate build-out). */157export function SemanticsLabel({ semantics, className, prefix }: { semantics: string | null | undefined; className?: string; prefix?: string }) {158  const label = semanticsLabel(semantics);159  if (!label) return null;160  return (161    <span className={cn("label-caps inline-flex items-center gap-1 normal-case tracking-normal text-[11px] text-ink-3", className)} title="Capacity semantics — what the published figure measures">162      {prefix && <span className="text-ink-4">{prefix}</span>}163      {label}164    </span>165  );166}167168/** Event significance band (major / medium / minor) — 3 bars, accent only for major. */169export function SignificanceBadge({ value, band, size = "xs", className, showLabel = true }: { value: number; band?: Significance | null; size?: "xs" | "sm"; className?: string; showLabel?: boolean }) {170  const b = significanceBand(value, band);171  const level = b === "major" ? 3 : b === "medium" ? 2 : 1;172  return (173    <Badge tone={b === "major" ? "accent" : "outline"} size={size} className={className} title={`${SIGNIFICANCE_LABEL[b]} significance (${value}/100)`}>174      <span className="inline-flex items-end gap-[2px]" aria-hidden>175        {[0, 1, 2].map((i) => (176          <span key={i} className={cn("inline-block w-[3px] rounded-[1px]", i === 0 ? "h-[5px]" : i === 1 ? "h-[7px]" : "h-[9px]", i < level ? (b === "major" ? "bg-accent" : "bg-ink-2") : "bg-hair-2")} />177        ))}178      </span>179      {showLabel && <span>{SIGNIFICANCE_LABEL[b]}</span>}180    </Badge>181  );182}183184/** AI marker with evidence level (confirmed / likely / associated). Plain "AI" when the level is unknown. */185export function AiMark({ evidence, size = "xs", className }: { evidence?: AiEvidence | null; size?: "xs" | "sm"; className?: string }) {186  const lvl = evidence && evidence !== "unknown" ? evidence : null;187  return (188    <Badge tone="accent" size={size} className={className} title={lvl ? AI_EVIDENCE_LABEL[lvl] : "AI / HPC infrastructure"}>189      <span aria-hidden className="inline-block size-[6px] rotate-45 bg-accent" />190      AI{lvl && lvl !== "confirmed" && <span className="font-normal opacity-80">{lvl}</span>}191    </Badge>192  );193}194195/** Project lifecycle stage chip (funnel vocabulary: Announced → Permitting → Approved → Construction → Opened). */196export function StageBadge({ stage, size = "xs", className }: { stage: ProjectStage; size?: "xs" | "sm"; className?: string }) {197  const tone = STATUS_TONE[stage as FacilityStatus] ?? "unknown";198  return (199    <span200      className={cn("inline-flex items-center gap-1.5 whitespace-nowrap rounded-xs border bg-surface font-medium leading-none text-ink-2", size === "xs" ? "h-[18px] px-1.5 text-[10.5px]" : "h-[22px] px-2 text-[11.5px]", className)}201      style={{ borderColor: `color-mix(in oklab, ${statusVar(tone)} 40%, var(--hair))` }}202      title={STAGE_LABEL[stage]}203    >204      <StatusDot status={stage as FacilityStatus} size={7} />205      {STAGE_LABEL[stage]}206    </span>207  );208}209210/** Authority tier letter (A–E) for THIS field — never a universal source rank. */211export function TierMark({ tier, className }: { tier: AuthorityTier; className?: string }) {212  return (213    <span className={cn("figure inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-xs border border-hair-2 px-1 text-[10.5px] font-medium text-ink-2", className)} title={AUTHORITY_TIER_LABEL[tier]}>214      {tier}215    </span>216  );217}218