veza/apps/web/src/components/ui/slider.tsx
senke 09bb663659 chore: enable noUncheckedIndexedAccess, isolate ghost MSW handlers, document go-clamd tech debt
- Enable TypeScript noUncheckedIndexedAccess and fix 133 resulting errors
  across 46 files with proper null guards, optional chaining, and fallbacks
- Extract education/gamification ghost feature MSW handlers into handlers-ghost.ts
- Add Storybook test plugin documentation in vitest.config.ts
- Document abandoned go-clamd dependency (2017) as tech debt in upload_validator.go

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

170 lines
4.2 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) - min) / (max - min)) * 100;
return (
<div
className={cn(
'group relative flex w-full touch-none select-none items-center',
className,
)}
>
<div className="relative h-1 group-hover:h-1.5 w-full grow overflow-hidden rounded-full bg-muted transition-all duration-150">
<div
className="absolute h-full bg-primary transition-all duration-[var(--duration-fast)] shadow-slider-thumb group-hover:shadow-[0_0_8px_var(--primary)]"
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-primary bg-background ring-offset-background pointer-events-none shadow-slider-thumb',
'scale-0 opacity-0 group-hover:scale-100 group-hover:opacity-100 transition-all duration-150',
disabled && 'opacity-50',
)}
style={{ left: `calc(${percentage}% - 10px)` }}
/>
</div>
);
},
);
Slider.displayName = 'Slider';
export { Slider };