spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
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