veza/apps/web/src/components/feedback/Progress.tsx
senke 73e8372b0e refactor: Phase 7 — Clean up legacy components and remove dead tokens
- Bulk replace text-white → text-foreground across 116 component files
  (preserving text-white/ opacity variants)
- Remove hover-glow-cyan, shadow-card-glow-cyan, shadow-button-primary-glow
  classes from all components
- Replace --duration-normal/--duration-immersive/--duration-slow with
  --sumi-duration-normal/--sumi-duration-slow across 130+ files
- Replace --ease-out/--ease-in-out with --sumi-ease-out/--sumi-ease-in-out
- Replace focus:ring-blue-500 → focus:ring-primary (4 files)
- Remove hover:scale-105/110 and hover:-translate-y-1/0.5 transforms
  (SUMI anti-pattern: no scale on hover)
- Clean up stale kodo- references in comments

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

123 lines
3.2 KiB
TypeScript

import { useMemo } from 'react';
import { cn } from '@/lib/utils';
export interface ProgressProps {
value: number; // 0-100
max?: number;
variant?: 'linear' | 'circular';
showLabel?: boolean;
label?: string;
color?: string;
className?: string;
}
/**
* Composant Progress pour afficher la progression d'une opération.
*/
export function Progress({
value,
max = 100,
variant = 'linear',
showLabel = false,
label,
color,
className,
}: ProgressProps) {
const percentage = useMemo(() => {
const clampedValue = Math.max(0, Math.min(value, max));
return Math.round((clampedValue / max) * 100);
}, [value, max]);
if (variant === 'circular') {
const radius = 20;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - (percentage / 100) * circumference;
return (
<div
className={cn(
'relative inline-flex items-center justify-center',
className,
)}
>
<svg
className="w-16 h-16 transform -rotate-90"
viewBox="0 0 50 50"
role="progressbar"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={max}
>
{/* Background circle */}
<circle
cx="25"
cy="25"
r={radius}
fill="none"
stroke="currentColor"
strokeWidth="4"
className="text-muted"
/>
{/* Progress circle */}
<circle
cx="25"
cy="25"
r={radius}
fill="none"
stroke={color || 'currentColor'}
strokeWidth="4"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
className={cn(
'transition-all duration-[var(--sumi-duration-normal)] ease-in-out',
!color && 'text-primary',
)}
style={color ? { stroke: color } : undefined}
/>
</svg>
{showLabel && (
<span className="absolute inset-0 flex items-center justify-center text-sm font-medium">
{percentage}%
</span>
)}
</div>
);
}
return (
<div className={cn('w-full', className)}>
{(showLabel || label) && (
<div className="flex justify-between mb-1 text-sm">
{label && <span className="text-muted-foreground">{label}</span>}
{showLabel && (
<span className="text-muted-foreground font-medium">
{percentage}%
</span>
)}
</div>
)}
<div
className="w-full bg-muted rounded-full h-2 overflow-hidden"
role="progressbar"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={max}
aria-label={
label ? `${label}: ${percentage}%` : `Progress: ${percentage}%`
}
>
<div
className={cn(
'h-full rounded-full transition-all duration-[var(--sumi-duration-normal)] ease-in-out',
!color && 'bg-primary',
)}
style={{
width: `${percentage}%`,
backgroundColor: color,
}}
/>
</div>
</div>
);
}