Replace legacy text-kodo-cyan/border-kodo-cyan/bg-kodo-cyan with semantic text-primary/border-primary/bg-primary across 51 components. The brand primary color now uses the design system token, enabling proper theme adaptation. Covers UI primitives, search, dashboard, chat, playlists, settings, social, marketplace, and auth components. Co-authored-by: Cursor <cursoragent@cursor.com>
211 lines
6.4 KiB
TypeScript
211 lines
6.4 KiB
TypeScript
import React, { useState, useCallback, lazy, Suspense } from 'react';
|
|
// PERF: Lazy load react-easy-crop (composant volumineux ~100KB)
|
|
// Mock Cropper since react-easy-crop is missing
|
|
const Cropper = lazy(() =>
|
|
Promise.resolve({
|
|
default: (_props: any) => (
|
|
<div className="bg-kodo-graphite flex items-center justify-center h-full text-white">
|
|
Cropper Mock
|
|
</div>
|
|
),
|
|
}),
|
|
);
|
|
import { Button } from '@/components/ui/button';
|
|
import { X, ZoomIn, RotateCw, Check } from 'lucide-react';
|
|
import { LoadingSpinner } from './loading-spinner';
|
|
|
|
/**
|
|
* ImageCropperProps - Propriétés du composant ImageCropper
|
|
*
|
|
* @interface ImageCropperProps
|
|
*/
|
|
interface ImageCropperProps {
|
|
/**
|
|
* URL ou source de l'image à recadrer
|
|
*/
|
|
imageSrc: string;
|
|
|
|
/**
|
|
* Ratio d'aspect du recadrage
|
|
*
|
|
* - `1`: Ratio carré (pour avatars)
|
|
* - `3`: Ratio paysage (pour bannières)
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <ImageCropper imageSrc={image} aspectRatio={1} />
|
|
* ```
|
|
*/
|
|
aspectRatio: number;
|
|
|
|
/**
|
|
* Fonction appelée pour annuler le recadrage
|
|
*/
|
|
onCancel: () => void;
|
|
|
|
/**
|
|
* Fonction appelée lorsque le recadrage est terminé
|
|
*
|
|
* @param {any} croppedAreaPixels - Zone recadrée en pixels
|
|
*/
|
|
onCropComplete: (croppedAreaPixels: any) => void;
|
|
|
|
/**
|
|
* Si `true`, utilise un recadrage circulaire (pour avatars)
|
|
*
|
|
* @default false
|
|
*/
|
|
circularCrop?: boolean;
|
|
}
|
|
|
|
/**
|
|
* ImageCropper - Composant de recadrage d'image avec design system Kodo
|
|
*
|
|
* Composant modal pour recadrer des images avec support pour :
|
|
* - Zoom (1x à 3x)
|
|
* - Rotation (0° à 360°)
|
|
* - Recadrage circulaire ou rectangulaire
|
|
* - Ratio d'aspect personnalisable
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* // Recadrage d'avatar (carré, circulaire)
|
|
* <ImageCropper
|
|
* imageSrc={avatarSrc}
|
|
* aspectRatio={1}
|
|
* circularCrop={true}
|
|
* onCancel={() => setShowCropper(false)}
|
|
* onCropComplete={(area) => handleCrop(area)}
|
|
* />
|
|
* ```
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* // Recadrage de bannière (paysage, rectangulaire)
|
|
* <ImageCropper
|
|
* imageSrc={bannerSrc}
|
|
* aspectRatio={3}
|
|
* circularCrop={false}
|
|
* onCancel={() => setShowCropper(false)}
|
|
* onCropComplete={(area) => handleCrop(area)}
|
|
* />
|
|
* ```
|
|
*
|
|
* @component
|
|
* @param {ImageCropperProps} props - Propriétés du composant
|
|
* @returns {JSX.Element} Modal de recadrage avec contrôles
|
|
*/
|
|
|
|
export const ImageCropper: React.FC<ImageCropperProps> = ({
|
|
imageSrc,
|
|
aspectRatio,
|
|
onCancel,
|
|
onCropComplete,
|
|
circularCrop = false,
|
|
}) => {
|
|
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
|
const [zoom, setZoom] = useState(1);
|
|
const [rotation, setRotation] = useState(0);
|
|
const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
|
|
|
|
const onCropChange = (crop: { x: number; y: number }) => {
|
|
setCrop(crop);
|
|
};
|
|
|
|
const onCropCompleteHandler = useCallback(
|
|
(_croppedArea: any, croppedAreaPixels: any) => {
|
|
setCroppedAreaPixels(croppedAreaPixels);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const handleSave = () => {
|
|
onCropComplete(croppedAreaPixels);
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-kodo-void/95 backdrop-blur-sm">
|
|
<div className="relative w-full max-w-2xl bg-kodo-graphite border border-border rounded-xl shadow-2xl overflow-hidden flex flex-col h-[80vh]">
|
|
{/* Header */}
|
|
<div className="p-4 border-b border-border flex justify-between items-center bg-kodo-ink">
|
|
<h3 className="font-bold text-white flex items-center gap-2">
|
|
Edit Image
|
|
</h3>
|
|
<Button variant="ghost" size="icon" onClick={onCancel}>
|
|
<X className="w-5 h-5" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Cropper Area */}
|
|
<div className="relative flex-1 bg-black">
|
|
<Suspense
|
|
fallback={
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
<LoadingSpinner size="lg" text="Chargement du recadreur..." />
|
|
</div>
|
|
}
|
|
>
|
|
<Cropper
|
|
image={imageSrc}
|
|
crop={crop}
|
|
zoom={zoom}
|
|
rotation={rotation}
|
|
aspect={aspectRatio}
|
|
onCropChange={onCropChange}
|
|
onCropComplete={onCropCompleteHandler}
|
|
onZoomChange={setZoom}
|
|
cropShape={circularCrop ? 'round' : 'rect'}
|
|
showGrid={true}
|
|
/>
|
|
</Suspense>
|
|
</div>
|
|
|
|
{/* Controls */}
|
|
<div className="p-8 bg-kodo-ink border-t border-border space-y-4">
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-xs text-muted-foreground w-16">Zoom</span>
|
|
<ZoomIn className="w-4 h-4 text-muted-foreground" />
|
|
<input
|
|
type="range"
|
|
value={zoom}
|
|
min={1}
|
|
max={3}
|
|
step={0.1}
|
|
aria-labelledby="Zoom"
|
|
onChange={(e) => setZoom(Number(e.target.value))}
|
|
className="flex-1 h-1 bg-muted rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:bg-primary [&::-webkit-slider-thumb]:rounded-full"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-xs text-muted-foreground w-16">Rotate</span>
|
|
<RotateCw className="w-4 h-4 text-muted-foreground" />
|
|
<input
|
|
type="range"
|
|
value={rotation}
|
|
min={0}
|
|
max={360}
|
|
step={1}
|
|
aria-labelledby="Rotation"
|
|
onChange={(e) => setRotation(Number(e.target.value))}
|
|
className="flex-1 h-1 bg-muted rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:bg-primary [&::-webkit-slider-thumb]:rounded-full"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-4 pt-2">
|
|
<Button variant="ghost" onClick={onCancel}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
onClick={handleSave}
|
|
icon={<Check className="w-4 h-4" />}
|
|
>
|
|
Apply Crop
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|