veza/apps/web/src/components/ui/badge.tsx
senke fa56dfa77e refactor: Phase 3a — Global color class migration to SUMI semantics
- Replace all kodo-* color classes across ~100 TSX files:
  kodo-void → background, kodo-ink → card, kodo-graphite → muted,
  kodo-steel → muted-foreground, kodo-cyan → primary, kodo-magenta → destructive,
  kodo-lime → success, kodo-red → destructive, kodo-gold → warning
- Replace cyan-500, magenta-500, lime-500 default Tailwind colors with
  semantic equivalents (primary, destructive, success)
- Fix WaveformVisualizer hardcoded hex colors to SUMI values
- Delete global-effects.css (conflicting, redundant with index.css)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 01:51:49 +01:00

224 lines
5.4 KiB
TypeScript

import * as React from 'react';
import { X } from 'lucide-react';
import { cn } from '@/lib/utils';
/**
* BadgeProps - Propriétés du composant Badge
*
* @interface BadgeProps
* @extends React.HTMLAttributes<HTMLSpanElement>
*/
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
/**
* Texte du badge (alternative à children)
*/
label?: string;
/**
* Variant de couleur du badge
*
* - `cyan`: Cyan (par défaut)
* - `magenta`: Magenta
* - `lime`: Lime (vert)
* - `gold`: Or
* - `terminal`: Style terminal (monospace)
* - `default`, `primary`: Alias pour cyan
* - `success`: Alias pour lime
* - `warning`: Alias pour gold
* - `error`: Alias pour magenta
* - `secondary`: Alias pour magenta
*
* @default 'cyan'
*/
variant?:
| 'cyan'
| 'magenta'
| 'lime'
| 'gold'
| 'terminal'
| 'default'
| 'primary'
| 'success'
| 'warning'
| 'error'
| 'secondary';
/**
* Icône à afficher dans le badge
*/
icon?: React.ReactNode;
/**
* Taille du badge
*
* - `sm`: Petit (px-2 py-0.5 text-[10px])
* - `md`: Moyen (px-2.5 py-0.5 text-[10px]) - par défaut
* - `lg`: Grand (px-4 py-1 text-xs)
*
* @default 'md'
*/
size?: 'sm' | 'md' | 'lg';
/**
* Si `true`, affiche un point au lieu du texte
*
* @default false
*/
dot?: boolean;
/**
* Nombre à afficher dans le badge (pour notifications, etc.)
* Si fourni et > 0, affiche le nombre dans un badge secondaire
*/
count?: number;
/**
* Contenu du badge (alternative à label)
*/
children?: React.ReactNode;
/**
* Callback pour supprimer/fermer le badge.
* Quand défini, affiche un bouton de fermeture (X).
*/
onDismiss?: () => void;
/**
* Si `true`, ajoute une animation de pulsation au badge.
*
* @default false
*/
pulse?: boolean;
}
/**
* Badge - Composant de badge avec design system Kodo
*
* Composant de badge pour afficher des labels, statuts ou compteurs.
* Supporte plusieurs variants de couleur, tailles et options d'affichage.
*
* @example
* ```tsx
* // Badge simple
* <Badge label="Nouveau" />
*
* // Badge avec variant
* <Badge variant="success">Actif</Badge>
*
* // Badge avec icône
* <Badge icon={<Star />} label="Premium" />
*
* // Badge avec compteur
* <Badge count={5}>Notifications</Badge>
* ```
*
* @component
* @param {BadgeProps} props - Propriétés du composant
* @returns {JSX.Element} Élément span stylisé comme un badge
*/
export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
(
{
label,
variant = 'cyan',
icon,
size = 'md',
dot,
count,
children,
className,
onDismiss,
pulse,
...props
},
ref,
) => {
// Map compatibility variants to Kodo variants
const variantMap: Record<string, string> = {
default: 'cyan',
primary: 'cyan',
success: 'lime',
warning: 'gold',
error: 'magenta',
secondary: 'magenta',
};
const actualVariant = variantMap[variant] || variant;
const styles: Record<string, string> = {
cyan: 'bg-muted/10 text-muted-foreground border-border/30',
magenta: 'bg-destructive/10 text-destructive border-destructive/30',
lime: 'bg-success/10 text-success border-success/30',
gold: 'bg-warning/10 text-warning border-warning/30',
terminal:
'bg-success/10 text-success border-success/30 font-mono',
};
const sizeStyles = {
sm: 'px-2 py-0.5 text-xs',
md: 'px-2.5 py-0.5 text-xs',
lg: 'px-4 py-1 text-xs',
};
const dotBgStyles: Record<string, string> = {
cyan: 'bg-muted-foreground',
magenta: 'bg-destructive',
lime: 'bg-success',
gold: 'bg-warning',
terminal: 'bg-success',
};
const displayText = label || children;
const badgeVariant = actualVariant as keyof typeof styles;
// Dot-only mode: colored circle without text
if (dot && !displayText && !icon && count === undefined) {
return (
<span
ref={ref}
className={cn(
'inline-block h-2 w-2 rounded-full',
dotBgStyles[badgeVariant] || dotBgStyles.cyan,
pulse && 'animate-pulse',
className,
)}
{...props}
/>
);
}
return (
<span
ref={ref}
className={cn(
'inline-flex items-center gap-1.5 rounded-full font-bold uppercase tracking-widest border',
styles[badgeVariant] || styles.cyan,
sizeStyles[size],
pulse && 'animate-pulse',
className,
)}
{...props}
>
{dot && <span className="w-3 h-3 rounded-full bg-current" />}
{icon && <span className="w-3 h-3">{icon}</span>}
{displayText}
{count !== undefined && count > 0 && (
<span className="ml-1 px-1.5 py-0.5 rounded-full bg-current/20 text-xs">
{count}
</span>
)}
{onDismiss && (
<button
type="button"
onClick={(e) => { e.stopPropagation(); onDismiss(); }}
className="ml-1 -mr-0.5 inline-flex items-center justify-center rounded-full p-0.5 hover:bg-black/10 transition-colors"
aria-label="Remove"
>
<X className="h-3 w-3" />
</button>
)}
</span>
);
},
);
Badge.displayName = 'Badge';