/** * Badge (web). A small pill for status, tags, and counts. Tone-driven: * accent for emphasis, semantic tones for status, ghost for a dashed * outline. `soft` fills with a tinted background instead of a border. * * Promoted * ATS-safe * OTIF */ import type { CSSProperties, ReactNode } from "react"; import { useTheme } from "@plyxui/styles"; import { radius, spacing } from "@plyxui/core"; export type BadgeTone = "neutral" | "accent" | "success" | "warning" | "danger" | "ghost"; export type BadgeSize = "sm" | "md"; const sizeMap: Record = { sm: { fontSize: 11, padY: 2, padX: spacing[2] }, md: { fontSize: 12, padY: 4, padX: spacing[3] }, }; export interface BadgeProps { tone?: BadgeTone; size?: BadgeSize; /** Filled tinted background instead of a bordered outline. */ soft?: boolean; leading?: ReactNode; className?: string; style?: CSSProperties; children?: ReactNode; } function withAlpha(hex: string, a: number): string { const n = parseInt(hex.slice(1), 16); return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`; } export function Badge({ tone = "neutral", size = "md", soft, leading, className, style, children }: BadgeProps) { const { colors } = useTheme(); const dims = sizeMap[size]; const toneColor: Record = { neutral: colors.textMuted, accent: colors.primaryOrange, success: colors.statusSuccess, warning: colors.statusWarning, danger: colors.statusError, ghost: colors.textMuted, }; const fg = toneColor[tone]; const base: CSSProperties = { display: "inline-flex", alignItems: "center", gap: 5, fontSize: dims.fontSize, fontWeight: 600, lineHeight: 1.2, padding: `${dims.padY}px ${dims.padX}px`, borderRadius: radius.pill, whiteSpace: "nowrap", color: tone === "neutral" ? colors.text : fg, background: soft ? withAlpha(fg, 0.12) : "transparent", border: soft ? "1px solid transparent" : `1px ${tone === "ghost" ? "dashed" : "solid"} ${tone === "neutral" ? colors.stroke : withAlpha(fg, 0.4)}`, ...style, }; return ( {leading} {children} ); }