import { useState, type ReactNode } from "react"; import { ART_BASE_URL } from "../map/art/load"; import { Icon, type IconName } from "./icons"; import type { Tone } from "./Panel"; const PILL_TONES: Record = { default: "border-space-500 bg-space-800 text-ink-200", muted: "border-space-600 bg-space-900 text-ink-400", amber: "border-amber-400/50 bg-amber-500/15 text-amber-200", cyan: "border-cyan-400/50 bg-cyan-500/15 text-cyan-200", emerald: "border-emerald-400/50 bg-emerald-500/15 text-emerald-200", rose: "border-rose-400/50 bg-rose-500/15 text-rose-200", fuchsia: "border-fuchsia-400/50 bg-fuchsia-500/15 text-fuchsia-200", }; /** A small status or tag chip. */ export function Pill({ tone = "default", icon, className = "", children, }: { tone?: Tone; icon?: IconName; className?: string; children: ReactNode; }) { return ( {icon && } {children} ); } const METER_FILLS: Partial> = { amber: "from-amber-500 to-amber-300 shadow-[0_0_10px] shadow-amber-400/60", cyan: "from-cyan-500 to-sky-300 shadow-[0_0_10px] shadow-cyan-400/60", emerald: "from-emerald-500 to-emerald-300 shadow-[0_0_10px] shadow-emerald-400/60", rose: "from-rose-600 to-rose-400 shadow-[0_0_10px] shadow-rose-500/60", fuchsia: "from-fuchsia-500 to-fuchsia-300", }; /** A labelled `progressbar`. `label` is its accessible name. */ export function Meter({ label, value, min = 0, max = 100, tone = "cyan", size = "md", className = "", }: { label: string; value: number; min?: number; max?: number; tone?: Tone; size?: "sm" | "md"; className?: string; }) { const percent = max > min ? Math.max(0, Math.min(100, ((value - min) / (max - min)) * 100)) : 0; return (
); } const STAT_VALUE_TONES: Partial> = { amber: "text-amber-300", cyan: "text-cyan-200", emerald: "text-emerald-300", rose: "text-rose-300", }; /** A label over a big number, with an optional icon. */ export function StatTile({ label, value, icon, tone = "default", valueLabel, className = "", }: { label: ReactNode; value: ReactNode; icon?: IconName; tone?: Tone; /** Accessible name of the value, when tests or screen readers need it. */ valueLabel?: string; className?: string; }) { return (
{icon && } {label}
{value}
); } /** The page title block. `title` is the screen's level-1 heading. */ export function ScreenHeader({ title, eyebrow, subtitle, icon, actions, }: { title: string; eyebrow?: string; subtitle?: ReactNode; icon?: IconName; actions?: ReactNode; }) { return (
{icon && ( )}
{eyebrow && (
{eyebrow}
)}

{title}

{subtitle &&

{subtitle}

}
{actions}
); } /** A generated map sprite's URL (decision 0.5.50). */ export function spriteUrl(sprite: string, density: "1x" | "2x" = "2x"): string { return `${ART_BASE_URL}/${sprite}@${density}.png`; } /** * Ship or location art. Decorative: the name is always in text beside it, so * `alt` is empty. A missing image hides itself (decision 0.5.50). */ export function Art({ sprite, className = "size-12" }: { sprite: string; className?: string }) { const [failed, setFailed] = useState(false); if (failed) return null; return ( setFailed(true)} className={`object-contain select-none ${className}`} /> ); }