refactor(ui): migrar componente Badge a HeroUI

This commit is contained in:
Miguel Ortega committed 2026-09-11 20:24:42 -05:00
1 parent ac1249084a
commit 78f0f6d50a
1 file changed
+103 -104
+103 -104
View File
@@ -1,18 +1,49 @@
"use client"; "use client";
import React from 'react'; import React from 'react';
import { Chip as HeroUIChip, ChipProps as HeroUIChipProps } from '@heroui/react';
import { tv } from '@heroui/styles';
import { FiX } from 'react-icons/fi'; import { FiX } from 'react-icons/fi';
export interface BadgeProps { // 1. Organizamos los estilos con 'tv' usando variantes estándar
children: React.ReactNode; const badgeStyles = tv({
variant?: base: "inline-flex items-center select-none border transition-colors",
| 'default' variants: {
| 'primary' customVariant: {
| 'secondary' default: 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-800/80 dark:text-slate-200 dark:border-slate-700',
| 'success' primary: 'bg-cyan-50 text-cyan-700 border-cyan-200 dark:bg-cyan-950/60 dark:text-cyan-300 dark:border-cyan-500/30',
| 'warning' secondary: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950/60 dark:text-purple-300 dark:border-purple-500/30',
| 'danger' success: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/60 dark:text-emerald-300 dark:border-emerald-500/30',
| 'info' warning: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/60 dark:text-amber-300 dark:border-amber-500/30',
| 'outline'; danger: 'bg-rose-50 text-rose-700 border-rose-200 dark:bg-rose-950/60 dark:text-rose-300 dark:border-rose-500/30',
info: 'bg-sky-50 text-sky-700 border-sky-200 dark:bg-sky-950/60 dark:text-sky-300 dark:border-sky-500/30',
outline: 'bg-transparent text-slate-700 border-slate-300 dark:text-slate-300 dark:border-slate-700',
},
customSize: {
sm: 'text-[10px] px-2 py-0.5 gap-1 rounded-md font-semibold h-auto min-w-min',
md: 'text-xs px-2.5 py-1 gap-1.5 rounded-lg font-medium h-auto min-w-min',
lg: 'text-sm px-3 py-1.5 gap-2 rounded-xl font-medium h-auto min-w-min',
}
},
defaultVariants: {
customVariant: "default",
customSize: "md",
}
});
// Mapeo auxiliar de colores para los dots según la variante
const dotColorMap: Record<NonNullable<BadgeProps['variant']>, string> = {
default: 'bg-slate-500',
primary: 'bg-cyan-500',
secondary: 'bg-purple-500',
success: 'bg-emerald-500',
warning: 'bg-amber-500',
danger: 'bg-rose-500',
info: 'bg-sky-500',
outline: 'bg-slate-400',
};
export interface BadgeProps extends Omit<HeroUIChipProps, 'variant' | 'size' | 'color' | 'className' | 'children'> {
variant?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'outline';
size?: 'sm' | 'md' | 'lg'; size?: 'sm' | 'md' | 'lg';
dot?: boolean; dot?: boolean;
dotPulse?: boolean; dotPulse?: boolean;
@@ -20,103 +51,71 @@ export interface BadgeProps {
removable?: boolean; removable?: boolean;
onRemove?: () => void; onRemove?: () => void;
className?: string; className?: string;
children?: React.ReactNode;
} }
const variantStyles: Record<NonNullable<BadgeProps['variant']>, { badge: string; dot: string }> = { export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
default: { (
badge: 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-800/80 dark:text-slate-200 dark:border-slate-700', {
dot: 'bg-slate-500', children,
}, variant = 'default',
primary: { size = 'md',
badge: 'bg-cyan-50 text-cyan-700 border-cyan-200 dark:bg-cyan-950/60 dark:text-cyan-300 dark:border-cyan-500/30', dot = false,
dot: 'bg-cyan-500', dotPulse = false,
}, icon,
secondary: { removable = false,
badge: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950/60 dark:text-purple-300 dark:border-purple-500/30', onRemove,
dot: 'bg-purple-500', className = '',
}, ...props
success: { },
badge: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/60 dark:text-emerald-300 dark:border-emerald-500/30', ref
dot: 'bg-emerald-500', ) => {
}, const dotColor = dotColorMap[variant];
warning: {
badge: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/60 dark:text-amber-300 dark:border-amber-500/30',
dot: 'bg-amber-500',
},
danger: {
badge: 'bg-rose-50 text-rose-700 border-rose-200 dark:bg-rose-950/60 dark:text-rose-300 dark:border-rose-500/30',
dot: 'bg-rose-500',
},
info: {
badge: 'bg-sky-50 text-sky-700 border-sky-200 dark:bg-sky-950/60 dark:text-sky-300 dark:border-sky-500/30',
dot: 'bg-sky-500',
},
outline: {
badge: 'bg-transparent text-slate-700 border-slate-300 dark:text-slate-300 dark:border-slate-700',
dot: 'bg-slate-400',
},
};
const sizeStyles = { return (
sm: 'text-[10px] px-2 py-0.5 gap-1 rounded-md font-semibold', <HeroUIChip
md: 'text-xs px-2.5 py-1 gap-1.5 rounded-lg font-medium', ref={ref}
lg: 'text-sm px-3 py-1.5 gap-2 rounded-xl font-medium', // En HeroUI v3 se usa className en singular (soporta string directo desde tv)
}; className={badgeStyles({
customVariant: variant,
customSize: size,
className
})}
{...props}
>
{/* Optional Dot indicator */}
{dot && (
<span className="relative flex h-1.5 w-1.5 shrink-0">
{dotPulse && (
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${dotColor}`} />
)}
<span className={`relative inline-flex rounded-full h-1.5 w-1.5 ${dotColor}`} />
</span>
)}
export const Badge: React.FC<BadgeProps> = ({ {/* Optional Icon */}
children, {icon && <span className="shrink-0">{icon}</span>}
variant = 'default',
size = 'md',
dot = false,
dotPulse = false,
icon,
removable = false,
onRemove,
className = '',
}) => {
const currentVariant = variantStyles[variant];
return ( {/* Content */}
<span <span>{children}</span>
className={`
inline-flex items-center select-none border transition-colors
${currentVariant.badge}
${sizeStyles[size]}
${className}
`}
>
{/* Optional Dot indicator */}
{dot && (
<span className="relative flex h-1.5 w-1.5 shrink-0">
{dotPulse && (
<span
className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${currentVariant.dot}`}
/>
)}
<span className={`relative inline-flex rounded-full h-1.5 w-1.5 ${currentVariant.dot}`} />
</span>
)}
{/* Optional Icon */} {/* Optional Remove Button */}
{icon && <span className="shrink-0">{icon}</span>} {removable && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onRemove?.();
}}
className="ml-0.5 p-0.5 rounded-full hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
aria-label="Remover etiqueta"
>
<FiX className="text-xs" />
</button>
)}
</HeroUIChip>
);
}
);
{/* Content */} Badge.displayName = 'Badge';
<span>{children}</span>
{/* Optional Remove Button */}
{removable && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onRemove?.();
}}
className="ml-0.5 p-0.5 rounded-full hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
aria-label="Remover etiqueta"
>
<FiX className="text-xs" />
</button>
)}
</span>
);
};