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";
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';
export interface BadgeProps {
children: React.ReactNode;
variant?:
| 'default'
| 'primary'
| 'secondary'
| 'success'
| 'warning'
| 'danger'
| 'info'
| 'outline';
// 1. Organizamos los estilos con 'tv' usando variantes estándar
const badgeStyles = tv({
base: "inline-flex items-center select-none border transition-colors",
variants: {
customVariant: {
default: 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-800/80 dark:text-slate-200 dark:border-slate-700',
primary: 'bg-cyan-50 text-cyan-700 border-cyan-200 dark:bg-cyan-950/60 dark:text-cyan-300 dark:border-cyan-500/30',
secondary: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950/60 dark:text-purple-300 dark:border-purple-500/30',
success: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/60 dark:text-emerald-300 dark:border-emerald-500/30',
warning: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/60 dark:text-amber-300 dark:border-amber-500/30',
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';
dot?: boolean;
dotPulse?: boolean;
@@ -20,103 +51,71 @@ export interface BadgeProps {
removable?: boolean;
onRemove?: () => void;
className?: string;
children?: React.ReactNode;
}
const variantStyles: Record<NonNullable<BadgeProps['variant']>, { badge: string; dot: string }> = {
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',
},
primary: {
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: 'bg-cyan-500',
},
secondary: {
badge: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950/60 dark:text-purple-300 dark:border-purple-500/30',
dot: 'bg-purple-500',
},
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',
dot: 'bg-emerald-500',
},
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',
},
};
export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
(
{
children,
variant = 'default',
size = 'md',
dot = false,
dotPulse = false,
icon,
removable = false,
onRemove,
className = '',
...props
},
ref
) => {
const dotColor = dotColorMap[variant];
const sizeStyles = {
sm: 'text-[10px] px-2 py-0.5 gap-1 rounded-md font-semibold',
md: 'text-xs px-2.5 py-1 gap-1.5 rounded-lg font-medium',
lg: 'text-sm px-3 py-1.5 gap-2 rounded-xl font-medium',
};
return (
<HeroUIChip
ref={ref}
// 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> = ({
children,
variant = 'default',
size = 'md',
dot = false,
dotPulse = false,
icon,
removable = false,
onRemove,
className = '',
}) => {
const currentVariant = variantStyles[variant];
{/* Optional Icon */}
{icon && <span className="shrink-0">{icon}</span>}
return (
<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>
)}
{/* Content */}
<span>{children}</span>
{/* Optional Icon */}
{icon && <span className="shrink-0">{icon}</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>
)}
</HeroUIChip>
);
}
);
{/* Content */}
<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>
);
};
Badge.displayName = 'Badge';