import type { HTMLAttributes, ReactNode } from "react"; import { DatabaseZap } from "lucide-react"; import { relative } from "@/lib/format"; import { cn } from "@/lib/utils"; export function Panel({ className, children, ...props }: HTMLAttributes) { return (
{children}
); } export function SectionHeader({ icon, title, subtitle, right, }: { icon?: ReactNode; title: string; subtitle?: string; right?: ReactNode; }) { return (
{icon && ( {icon} )}

{title}

{subtitle &&

{subtitle}

}
{right}
); } export function StatCard({ label, value, hint, tone = "default", icon, }: { label: string; value: ReactNode; hint?: string; tone?: "default" | "emerald" | "amber" | "red" | "cyan"; icon?: ReactNode; }) { const tones: Record = { default: "text-zinc-100", emerald: "text-emerald-400", amber: "text-amber-400", red: "text-red-400", cyan: "text-cyan-400", }; return (
{label} {icon && {icon}}
{value} {hint && {hint}}
); } export function Spinner({ label = "Loading" }: { label?: string }) { return (
{label}…
); } export function ErrorState({ message }: { message: string }) { return (

Could not load data

{message}

); } export function EmptyState({ message }: { message: string }) { return (
{message}
); } /** One-line "data as of X" freshness indicator for batch sections. */ export function FreshnessLine({ asOf, className }: { asOf: string | null; className?: string }) { return ( {asOf ? `Gold data as of ${relative(asOf)}` : "Gold data freshness unknown"} ); } /** Banner shown when flight history is empty: explains why, keeps live context. */ export function BatchEmptyState({ compact = false }: { compact?: boolean }) { return (

No flight history in the warehouse yet

Delay, punctuality and traffic charts read OpenSky flight movements. OpenSky publishes them on a nightly batch with a limited daily credit budget, so this table fills as the collector's polls land — not instantly. Live airspace below is unaffected.

); } export function ProgressBar({ value, tone = "emerald" }: { value: number; tone?: string }) { const width = Math.max(0, Math.min(100, value * 100)); const colors: Record = { emerald: "bg-emerald-500", amber: "bg-amber-500", red: "bg-red-500", cyan: "bg-cyan-500", }; return (
); }