import Link from 'next/link'; import type { ReactNode } from 'react'; import { cn } from '@/lib/cn'; import { fmtAgo } from '@/lib/format'; import { routes } from '@/lib/site'; import { FreshnessBadge } from '@/components/ui/badges'; import type { StatsSnapshot } from '@/lib/types'; /** "derived" tag with a link to the methodology page — mandatory next to every computed metric. */ export function Derived({ className, what }: { className?: string; what?: string }) { return ( derived ); } /** Short explanatory note under a chart or table. `tone="warn"` renders a left rule for disclaimers. */ export function Note({ children, tone = 'muted', className }: { children: ReactNode; tone?: 'muted' | 'warn'; className?: string }) { return (

{children}

); } /** Prominent disclaimer block (debris / reentries pages). Text is shown verbatim from the API. */ export function Disclaimer({ text, label = 'Disclaimer' }: { text: string; label?: string }) { return (

{label}

{text}

); } /** Responsive grid of `Stat` tiles separated by a hairline on top. */ export function StatGrid({ children, cols = 4, className }: { children: ReactNode; cols?: 3 | 4 | 5; className?: string }) { const c = cols === 3 ? 'sm:grid-cols-3' : cols === 5 ? 'sm:grid-cols-3 lg:grid-cols-5' : 'sm:grid-cols-3 lg:grid-cols-4'; return
{children}
; } /** Group title inside a section (e.g. "Catalogue", "Decays"). */ export function GroupTitle({ children, hint }: { children: ReactNode; hint?: ReactNode }) { return (

{children}

{hint && {hint}}
); } /** Chart block: small heading + optional note. No card chrome — hairline composition. */ export function ChartBlock({ title, hint, children, note, derived, className }: { title: ReactNode; hint?: ReactNode; children: ReactNode; note?: ReactNode; derived?: boolean; className?: string }) { return (

{title} {derived && }

{hint && {hint}}
{children} {note &&
{typeof note === 'string' ? {note} : note}
}
); } /** Two-column layout on desktop, stacked on mobile (DOM order = visual order). */ export function TwoCol({ children, className }: { children: ReactNode; className?: string }) { return
{children}
; } /** Horizontal chip list (filters / metric selector). Scrolls on mobile without breaking layout. */ export function Chips({ items, className, ariaLabel }: { items: { href: string; label: string; active: boolean; count?: string }[]; className?: string; ariaLabel: string }) { return ( ); } /** Rank number cell. */ export function Rank({ n, className }: { n: number; className?: string }) { return {String(n).padStart(2, '0')}; } /** Inline rank prefix inside the primary cell of ranked tables (works in stacked mode: `.data-table.stack td` forces display:block, so a hidden rank column would reappear on mobile). */ export function InlineRank({ n }: { n: number }) { return ; } type Connector = StatsSnapshot['connectors'][number]; export function connectorFreshness(c: Connector, now = Date.now()): 'fresh' | 'aging' | 'stale' | 'unavailable' | 'not_enabled' { if (!c.enabled) return 'not_enabled'; if (!c.last_success_at) return 'unavailable'; const t = new Date(c.last_success_at).getTime(); if (Number.isNaN(t)) return 'unavailable'; const age = (now - t) / 1000; if (age < 3 * c.interval_seconds) return 'fresh'; if (age < 12 * c.interval_seconds) return 'aging'; return 'stale'; } /** Connector freshness strip — one line per connector. */ export function ConnectorStrip({ connectors }: { connectors: Connector[] }) { if (!connectors.length) return null; const now = Date.now(); return ( ); }