- 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>
170 lines
4.2 KiB
TypeScript
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 };
|