spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1import type { ReactNode } from 'react';2import { cn } from '@/lib/cn';3import { eventStyle, subtypeLabel } from '@/lib/event-styles';4import { importanceSteps, significanceBand } from '@/lib/format';5import { CONFIDENCE_LABELS, SENSOR_TIER_LABELS } from '@/lib/site';67const base = 'inline-flex items-center gap-1 whitespace-nowrap rounded-[3px] px-1.5 py-[1px] text-[11px] font-medium leading-4 tracking-wide';89/** Event type chip with its fixed hue; `sub` appends the careful subtype label. */10export function EventTypeBadge({ type, subtype, className, small = false }: { type: string; subtype?: string | null; className?: string; small?: boolean }) {11 const s = eventStyle(type);12 return (13 <span className={cn(base, 'uppercase', small && 'px-1 text-[10px]', className)} style={{ color: s.color, background: s.soft }} data-event-type={s.key}>14 {s.label}15 {subtype && <span className="normal-case tracking-normal opacity-80">· {subtypeLabel(subtype)}</span>}16 </span>17 );18}1920const CONF: Record<string, string> = {21 VERIFIED: 'text-positive bg-positive-soft',22 HIGH_CONFIDENCE: 'text-positive bg-positive-soft',23 LIKELY: 'text-accent bg-accent-soft',24 INFERRED: 'text-warning bg-warning-soft',25 LOW_CONFIDENCE: 'text-danger bg-danger-soft',26};27/** Confidence label chip (spec §50). Renders nothing when missing — never invents a status. */28export function ConfidenceBadge({ label, value, className, withValue = false }: { label: string | null | undefined; value?: number | null; className?: string; withValue?: boolean }) {29 if (!label) return null;30 const k = label.toUpperCase();31 return (32 <span className={cn(base, CONF[k] ?? 'text-ink-2 bg-surface-2', className)} title={value !== null && value !== undefined ? `confidence ${(value * 100).toFixed(0)} %` : undefined}>33 {CONFIDENCE_LABELS[k] ?? label}34 {withValue && value !== null && value !== undefined && <span className="tnum opacity-75">{Math.round(value * 100)} %</span>}35 </span>36 );37}3839/** Importance as a stepped meter (▮▮▮▯). */40export function ImportanceMeter({ importance, className }: { importance: number | null | undefined; className?: string }) {41 const n = importanceSteps(importance);42 const pct = importance === null || importance === undefined ? null : Math.round((importance > 1 ? importance : importance * 100));43 return (44 <span className={cn('inline-flex items-center gap-[2px]', className)} title={pct === null ? 'importance unknown' : `importance ${pct} / 100`} aria-label={pct === null ? 'importance unknown' : `Importance ${pct} of 100`}>45 {[1, 2, 3].map((i) => (46 <span key={i} className={cn('block h-2.5 w-[3px] rounded-[1px]', i <= n ? (n === 3 ? 'bg-warning' : 'bg-ink-2') : 'bg-rule-strong')} />47 ))}48 </span>49 );50}5152const SIG: Record<string, string> = { noise: 'text-ink-3 bg-surface-2', minor: 'text-ink-2 bg-surface-2', meaningful: 'text-accent bg-accent-soft', major: 'text-warning bg-warning-soft', critical: 'text-danger bg-danger-soft' };53export function SignificanceBadge({ value, className }: { value: number | null | undefined; className?: string }) {54 const band = significanceBand(value);55 if (!band) return null;56 return (57 <span className={cn(base, SIG[band], className)}>58 {band} <span className="tnum opacity-75">{(value as number).toFixed(2)}</span>59 </span>60 );61}6263/** Sensor tier A–E (crawl cadence). */64export function SensorTierBadge({ tier, className, withLabel = false }: { tier: string | null | undefined; className?: string; withLabel?: boolean }) {65 if (!tier) return null;66 const t = tier.toLowerCase();67 return (68 <span className={cn(base, 'mono', className)} style={{ color: `var(--tier-${t})`, background: `color-mix(in srgb, var(--tier-${t}) 12%, transparent)` }} title={`Tier ${tier} · ${SENSOR_TIER_LABELS[tier] ?? ''}`}>69 {tier}70 {withLabel && <span className="font-sans tracking-normal">· {SENSOR_TIER_LABELS[tier]}</span>}71 </span>72 );73}7475/** Company tier 1–4 (importance band). */76export function CompanyTierBadge({ tier, className }: { tier: number | null | undefined; className?: string }) {77 if (!tier) return null;78 return (79 <span className={cn(base, 'mono bg-surface-2 text-ink-2', className)} title={`Company tier ${tier} (importance band, affects crawl priority only)`}>80 T{tier}81 </span>82 );83}8485const STATUS: Record<string, string> = {86 active: 'text-positive bg-positive-soft',87 ok: 'text-positive bg-positive-soft',88 healthy: 'text-positive bg-positive-soft',89 open: 'text-positive bg-positive-soft',90 listed: 'text-positive bg-positive-soft',91 current: 'text-positive bg-positive-soft',92 pending: 'text-accent bg-accent-soft',93 running: 'text-accent bg-accent-soft',94 review: 'text-warning bg-warning-soft',95 paused: 'text-warning bg-warning-soft',96 stale: 'text-warning bg-warning-soft',97 possibly_inactive: 'text-warning bg-warning-soft',98 failing: 'text-danger bg-danger-soft',99 failed: 'text-danger bg-danger-soft',100 dead: 'text-danger bg-danger-soft',101 blocked: 'text-danger bg-danger-soft',102 website_unavailable: 'text-danger bg-danger-soft',103 retracted: 'text-danger bg-danger-soft',104 retired: 'text-ink-3 bg-surface-2',105 duplicate: 'text-ink-3 bg-surface-2',106 no_longer_listed: 'text-ink-3 bg-surface-2',107 removed: 'text-ink-3 bg-surface-2',108 superseded: 'text-ink-3 bg-surface-2',109 done: 'text-ink-2 bg-surface-2',110};111const STATUS_LABEL: Record<string, string> = { no_longer_listed: 'no longer listed', removed: 'no longer listed', possibly_inactive: 'possibly inactive', website_unavailable: 'website unavailable' };112export function StatusBadge({ status, className }: { status: string | null | undefined; className?: string }) {113 if (!status) return null;114 const k = status.toLowerCase();115 return <span className={cn(base, STATUS[k] ?? 'text-ink-2 bg-surface-2', className)}>{STATUS_LABEL[k] ?? k.replace(/_/g, ' ')}</span>;116}117118/** ISO country chip (no flag emoji — consistent across platforms). */119export function CountryChip({ code, name, className, link = true }: { code: string | null | undefined; name?: string | null; className?: string; link?: boolean }) {120 if (!code) return null;121 const inner = (122 <span className={cn(base, 'mono bg-surface-2 text-ink-2', className)} title={name ?? undefined}>123 {code.toUpperCase()}124 </span>125 );126 if (!link) return inner;127 return (128 <a href={`/country/${code.toLowerCase()}`} className="hover:opacity-80" aria-label={`Country ${name ?? code}`}>129 {inner}130 </a>131 );132}133134export function Chip({ children, className, tone = 'neutral', title }: { children: ReactNode; className?: string; tone?: 'neutral' | 'accent' | 'positive' | 'warning' | 'danger' | 'outline'; title?: string }) {135 return (136 <span137 title={title}138 className={cn(139 base,140 tone === 'neutral' && 'bg-surface-2 text-ink-2',141 tone === 'accent' && 'bg-accent-soft text-accent',142 tone === 'positive' && 'bg-positive-soft text-positive',143 tone === 'warning' && 'bg-warning-soft text-warning',144 tone === 'danger' && 'bg-danger-soft text-danger',145 tone === 'outline' && 'border border-rule text-ink-3',146 className,147 )}148 >149 {children}150 </span>151 );152}153154export function OriginBadge({ origin, model, prompt }: { origin: string | null | undefined; model?: string | null; prompt?: string | null }) {155 if (!origin) return null;156 const llm = origin === 'llm' || origin === 'hybrid';157 return (158 <span className={cn(base, llm ? 'bg-warning-soft text-warning' : origin === 'backfill' ? 'bg-surface-2 text-ink-3' : 'bg-surface-2 text-ink-2')} title={llm && model ? `${model}${prompt ? ` · prompt ${prompt}` : ''}` : undefined}>159 {origin}160 {llm && model && <span className="mono font-normal opacity-80">· {model}</span>}161 </span>162 );163}164