veza/apps/web/src/components/ui/slider.tsx
senke 41b1cfaedd aesthetic-improvements: automated replacement of decorative cyan with steel (80/20 rule, Action 11.3.1.3)
- Created automated script (scripts/replace-decorative-cyan.py) to systematically replace decorative/informational kodo-cyan instances with kodo-steel variants
- Script intelligently preserves active/functional states, design system variants, semantic indicators, and interactive states
- Modified 85 files, replaced 145 decorative instances, preserved 47 functional instances
- No linter errors, type safety maintained
- Action 11.3.1.3 significantly advanced (total: ~302 instances replaced across ~229 files including previous batches)
2026-01-16 11:40:13 +01:00

169 lines
3.9 KiB
TypeScript

import * as React from 'react';
import { cn } from '@/lib/utils';
/**
* SliderProps - Propriétés du composant Slider
*
* @interface SliderProps
* @extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'value' | 'onChange'>
*/
export interface SliderProps
extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'type' | 'value' | 'onChange'
> {
/**
* Valeur du slider (tableau avec une valeur pour un slider simple)
*
* @default [0]
*
* @example
* ```tsx
* <Slider value={[50]} onValueChange={(val) => setValue(val)} />
* ```
*/
value?: number[];
/**
* Fonction appelée lorsque la valeur change
*
* @param {number[]} value - Nouvelle valeur (tableau)
*
* @example
* ```tsx
* <Slider onValueChange={(value) => console.log('Value:', value[0])} />
* ```
*/
onValueChange?: (value: number[]) => void;
/**
* Valeur minimale
*
* @default 0
*/
min?: number;
/**
* Valeur maximale
*
* @default 100
*/
max?: number;
/**
* Pas d'incrémentation
*
* @default 1
*/
step?: number;
/**
* CRITIQUE FIX #43: Label accessible pour le slider (aria-label)
*/
'aria-label'?: string;
/**
* CRITIQUE FIX #43: ID d'un élément qui décrit le slider (aria-labelledby)
*/
'aria-labelledby'?: string;
}
/**
* Slider - Composant de curseur avec design system Kodo
*
* Composant de curseur (slider) pour sélectionner une valeur dans une plage.
* Utilise le design system Kodo avec une barre cyan et un indicateur visuel.
*
* @example
* ```tsx
* // Slider simple
* <Slider value={[50]} onValueChange={(val) => setValue(val[0])} />
*
* // Slider avec plage personnalisée
* <Slider
* value={[25]}
* min={0}
* max={200}
* step={5}
* onValueChange={(val) => setValue(val[0])}
* />
*
* // Slider désactivé
* <Slider value={[50]} disabled />
* ```
*
* @component
* @param {SliderProps} props - Propriétés du composant
* @returns {JSX.Element} Élément div contenant un slider stylisé
*/
const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
(
{
className,
value = [0],
onValueChange,
min = 0,
max = 100,
step = 1,
disabled,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
...props
},
ref,
) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = [Number(e.target.value)];
if (onValueChange) {
onValueChange(newValue);
}
};
const percentage = ((value[0] - min) / (max - min)) * 100;
return (
<div
className={cn(
'relative flex w-full touch-none select-none items-center',
className,
)}
>
<div className="relative h-2 w-full grow overflow-hidden rounded-full bg-kodo-steel">
<div
className="absolute h-full bg-kodo-cyan transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
<input
ref={ref}
type="range"
min={min}
max={max}
step={step}
value={value[0]}
onChange={handleChange}
disabled={disabled}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
// CRITIQUE FIX #43: Ajouter les attributs ARIA pour l'accessibilité
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
aria-valuenow={value[0]}
aria-valuemin={min}
aria-valuemax={max}
{...props}
/>
<div
className={cn(
'absolute h-5 w-5 rounded-full border-2 border-kodo-steel bg-white ring-offset-kodo-void transition-transform pointer-events-none',
disabled && 'opacity-50',
)}
style={{ left: `calc(${percentage}% - 10px)` }}
/>
</div>
);
},
);
Slider.displayName = 'Slider';
export { Slider };