import type { ReactNode } from 'react'; /** * StatusBadge — single source of truth for status pills across the admin. * * Three usage modes (in increasing specificity): * * 1. Status-string mode (legacy, backward-compatible): * * Looks the status up in the canonical status→tone map. Unknown values * fall back to neutral slate. * * 2. Tone mode: * * * Use when your label needs a specific color but isn't a known status key * (e.g. translated labels, custom domain words). The tone palette is the * same one the status map maps into, so visuals stay consistent. * * 3. Tier preset: * * * For tier badges on the Addons page, Pricing tables, etc. * * Size variants: `size="xs"` for inline meta pills, default is the regular * status pill size used in tables. */ type Tone = 'success' | 'warning' | 'danger' | 'info' | 'premium' | 'brand' | 'neutral'; const TONE_CLASSES: Record = { success: 'bg-emerald-50 text-emerald-800 ring-emerald-600/20 dark:bg-emerald-950/50 dark:text-emerald-300 dark:ring-emerald-500/30', warning: 'bg-amber-50 text-amber-900 ring-amber-600/20 dark:bg-amber-950/40 dark:text-amber-200 dark:ring-amber-500/30', danger: 'bg-rose-50 text-rose-700 ring-rose-600/20 dark:bg-rose-950/40 dark:text-rose-200 dark:ring-rose-500/30', info: 'bg-sky-50 text-sky-900 ring-sky-600/20 dark:bg-sky-950/40 dark:text-sky-200 dark:ring-sky-500/30', premium: 'bg-violet-50 text-violet-800 ring-violet-600/20 dark:bg-violet-950/40 dark:text-violet-200 dark:ring-violet-500/30', brand: 'bg-brand-50 text-brand-800 ring-brand-600/20 dark:bg-brand-950/40 dark:text-brand-200 dark:ring-brand-500/30', neutral: 'bg-slate-100 text-slate-700 ring-slate-500/15 dark:bg-slate-800 dark:text-slate-300', }; /** * Status string → tone mapping. Domains that need additional status keys * (e.g. marketplace 'reversed', 'accrued') should add them here rather than * rolling a new component — single source of truth. */ const STATUS_TO_TONE: Record = { // Success publish: 'success', published: 'success', paid: 'success', completed: 'success', complete: 'success', success: 'success', active: 'success', available: 'success', approved: 'success', enabled: 'success', open: 'success', passed: 'success', // Warning / pending pending: 'warning', accrued: 'warning', unpaid: 'warning', locked: 'warning', draft_review: 'warning', needs_reply: 'warning', in_progress: 'warning', 'in-progress': 'warning', // Danger rejected: 'danger', reversed: 'danger', suspended: 'danger', cancelled: 'danger', canceled: 'danger', refunded: 'danger', failed: 'danger', trash: 'danger', disabled: 'danger', revoked: 'danger', closed: 'danger', // Info future: 'info', scheduled: 'info', processing: 'info', unlocked: 'info', // Premium private: 'premium', // Neutral (default) draft: 'neutral', 'on-hold': 'warning', on_hold: 'warning', }; const TIER_TO_TONE: Record = { free: { tone: 'success', label: 'Free' }, starter: { tone: 'warning', label: 'Starter' }, growth: { tone: 'brand', label: 'Growth' }, pro: { tone: 'brand', label: 'Growth' }, // tier-key alias used in some catalogs scale: { tone: 'premium', label: 'Scale' }, }; type Size = 'xs' | 'sm'; const SIZE_CLASSES: Record = { xs: 'rounded-full px-2 py-0.5 text-xs font-medium', sm: 'rounded-full px-2.5 py-0.5 text-xs font-medium', }; export type StatusBadgeProps = { /** Status string lookup (legacy). One of: paid, pending, refunded, etc. */ status?: string; /** Direct tone override; pairs with `label`. */ tone?: Tone; /** Tier preset (free / starter / growth / scale). Wins over `status`. */ tier?: keyof typeof TIER_TO_TONE; /** Explicit label. Defaults to humanized `status` or tier name. */ label?: ReactNode; /** Pill size. */ size?: Size; /** Lowercase the label or render as-is. Defaults to capitalize. */ caseStyle?: 'capitalize' | 'as-is'; }; export function StatusBadge({ status, tone, tier, label, size = 'sm', caseStyle = 'capitalize', }: StatusBadgeProps) { let resolvedTone: Tone = 'neutral'; let resolvedLabel: ReactNode = label; if (tier && TIER_TO_TONE[tier]) { resolvedTone = TIER_TO_TONE[tier].tone; if (resolvedLabel === undefined) { resolvedLabel = TIER_TO_TONE[tier].label; } } else if (tone) { resolvedTone = tone; if (resolvedLabel === undefined && status) { resolvedLabel = status.replace(/[-_]/g, ' '); } } else if (status) { resolvedTone = STATUS_TO_TONE[status.toLowerCase()] ?? 'neutral'; if (resolvedLabel === undefined) { resolvedLabel = status.replace(/[-_]/g, ' '); } } const caseCls = caseStyle === 'capitalize' ? 'capitalize' : ''; return ( {resolvedLabel} ); }