veza/apps/web/src/components/ui/progress.tsx
senke 503e6f00b6 feat(a11y): comprehensive accessibility & view states improvements
Sprint 1 — Quick A11y wins:
- progress.tsx: role=progressbar + aria-value* + aria-label
- switch.tsx: role=switch + aria-checked
- skeleton.tsx: aria-hidden=true
- alert.tsx, Toast.tsx, SelectTrigger.tsx: aria-labels on close buttons
- PostCard.tsx: alt on images + aria-labels on icon buttons
- ProductCard.tsx: aria-labels on play/view buttons
- modal.tsx: role=dialog + aria-modal + aria-labelledby
- input.tsx: error state + aria-invalid + aria-describedby
- FAB.tsx: forward aria-label from label prop

Sprint 2 — Structural A11y + View States:
- tabs/: full ARIA tablist/tab/tabpanel + arrow key navigation
- radio-group.tsx: role=radio + arrow key navigation
- select/: aria-activedescendant + full keyboard navigation
- List.tsx + card.tsx: focus-visible states on interactive elements
- DashboardPage, LibraryPage, LiveView, QueueView: error states
- WishlistView, AdminDashboard, AnalyticsView, SellerDashboard: loading/empty states

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 23:04:35 +01:00

208 lines
4.9 KiB
TypeScript

import * as React from 'react';
import { cn } from '@/lib/utils';
/**
* ProgressProps - Propriétés du composant Progress
*
* @interface ProgressProps
* @extends React.HTMLAttributes<HTMLDivElement>
*/
export interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Valeur actuelle de la progression (entre 0 et max)
*
* @example
* ```tsx
* <Progress value={75} max={100} />
* ```
*/
value: number;
/**
* Valeur maximale (par défaut 100)
*
* @default 100
*/
max?: number;
/**
* Variant du composant de progression
*
* - `default`: Barre de progression standard
* - `gaming`: Style gaming avec bordure dorée et effet glow
* - `segmented`: Non implémenté (réservé pour usage futur)
*
* @default 'default'
*/
variant?: 'default' | 'gaming' | 'segmented';
/**
* Couleur de la barre de progression
*
* - `cyan`: Cyan (par défaut)
* - `magenta`: Magenta
* - `lime`: Lime
* - `gold`: Or
*
* @default 'cyan'
*/
color?: 'cyan' | 'magenta' | 'lime' | 'gold';
/**
* Label accessible pour la barre de progression
*
* @default 'Progress'
*/
'aria-label'?: string;
/**
* Label à afficher à gauche de la barre
*
* @example
* ```tsx
* <Progress value={50} labelLeft="50%" labelRight="100%" />
* ```
*/
labelLeft?: string;
/**
* Label à afficher à droite de la barre
*
* @example
* ```tsx
* <Progress value={50} labelLeft="50%" labelRight="100%" />
* ```
*/
labelRight?: string;
}
/**
* Progress - Composant de barre de progression avec design system Kodo
*
* Composant de barre de progression pour afficher l'avancement d'une tâche.
* Supporte plusieurs variants et couleurs selon le design system Kodo.
*
* @example
* ```tsx
* // Barre de progression simple
* <Progress value={75} />
*
* // Barre avec labels
* <Progress
* value={60}
* labelLeft="60%"
* labelRight="100%"
* />
*
* // Style gaming
* <Progress
* value={85}
* variant="glass"
* labelLeft="HP"
* labelRight="100%"
* />
*
* // Avec couleur personnalisée
* <Progress value={50} color="magenta" />
* ```
*
* @component
* @param {ProgressProps} props - Propriétés du composant
* @returns {JSX.Element} Élément div contenant une barre de progression stylisée
*/
export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
(
{
value,
max = 100,
variant = 'default',
color = 'cyan',
'aria-label': ariaLabel = 'Progress',
labelLeft,
labelRight,
className,
...props
},
ref,
) => {
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
const colorStyles = {
cyan: 'bg-primary',
magenta: 'bg-kodo-magenta',
lime: 'bg-kodo-lime',
gold: 'bg-warning',
};
const gradientStyles = {
cyan: 'from-kodo-cyan to-kodo-cyan',
magenta: 'from-kodo-magenta to-purple-600',
lime: 'from-kodo-lime to-green-600',
gold: 'from-kodo-gold to-orange-500',
};
if (variant === 'gaming') {
return (
<div
className={cn('relative', className)}
ref={ref}
role="progressbar"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={max}
aria-label={ariaLabel}
{...props}
>
<div className="h-4 bg-kodo-void rounded-full overflow-hidden border border-warning/30">
<div
className={cn(
'h-full bg-gradient-to-r shadow-gold-glow transition-all duration-[var(--duration-slow)]',
gradientStyles.gold,
)}
style={{ width: `${percentage}%` }}
/>
</div>
{(labelLeft || labelRight) && (
<div className="flex justify-between text-xs font-mono font-bold text-warning mt-1 uppercase tracking-wider">
<span>{labelLeft}</span>
<span>{labelRight}</span>
</div>
)}
</div>
);
}
return (
<div
className={cn('w-full', className)}
ref={ref}
role="progressbar"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={max}
aria-label={ariaLabel}
{...props}
>
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className={cn(
'h-full transition-all duration-[var(--duration-normal)] shadow-slider-thumb',
colorStyles[color],
)}
style={{ width: `${percentage}%` }}
/>
</div>
{(labelLeft || labelRight) && (
<div className="flex justify-between text-xs text-muted-foreground mt-1 font-mono">
<span>{labelLeft}</span>
<span>{labelRight}</span>
</div>
)}
</div>
);
},
);
Progress.displayName = 'Progress';
// Alias for compatibility
export const ProgressBar = Progress;