2026-01-07 09:31:02 +00:00
|
|
|
import React, { useState, useCallback, lazy, Suspense } from 'react';
|
|
|
|
|
// PERF: Lazy load react-easy-crop (composant volumineux ~100KB)
|
2026-01-07 18:39:21 +00:00
|
|
|
// Mock Cropper since react-easy-crop is missing
|
2026-01-13 18:47:57 +00:00
|
|
|
const Cropper = lazy(() =>
|
|
|
|
|
Promise.resolve({
|
|
|
|
|
default: (_props: any) => (
|
2026-01-16 00:56:37 +00:00
|
|
|
<div className="bg-kodo-graphite flex items-center justify-center h-full text-white">
|
2026-01-13 18:47:57 +00:00
|
|
|
Cropper Mock
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-01-07 18:39:21 +00:00
|
|
|
import { Button } from '@/components/ui/button';
|
2026-01-07 09:31:02 +00:00
|
|
|
import { X, ZoomIn, RotateCw, Check } from 'lucide-react';
|
|
|
|
|
import { LoadingSpinner } from './loading-spinner';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ImageCropperProps - Propriétés du composant ImageCropper
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @interface ImageCropperProps
|
|
|
|
|
*/
|
|
|
|
|
interface ImageCropperProps {
|
|
|
|
|
/**
|
|
|
|
|
* URL ou source de l'image à recadrer
|
|
|
|
|
*/
|
|
|
|
|
imageSrc: string;
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Ratio d'aspect du recadrage
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* - `1`: Ratio carré (pour avatars)
|
|
|
|
|
* - `3`: Ratio paysage (pour bannières)
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* <ImageCropper imageSrc={image} aspectRatio={1} />
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
|
|
|
|
aspectRatio: number;
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Fonction appelée pour annuler le recadrage
|
|
|
|
|
*/
|
|
|
|
|
onCancel: () => void;
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Fonction appelée lorsque le recadrage est terminé
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @param {any} croppedAreaPixels - Zone recadrée en pixels
|
|
|
|
|
*/
|
|
|
|
|
onCropComplete: (croppedAreaPixels: any) => void;
|
2026-01-07 18:39:21 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
/**
|
|
|
|
|
* Si `true`, utilise un recadrage circulaire (pour avatars)
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @default false
|
|
|
|
|
*/
|
|
|
|
|
circularCrop?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ImageCropper - Composant de recadrage d'image avec design system Kodo
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* Composant modal pour recadrer des images avec support pour :
|
|
|
|
|
* - Zoom (1x à 3x)
|
|
|
|
|
* - Rotation (0° à 360°)
|
|
|
|
|
* - Recadrage circulaire ou rectangulaire
|
|
|
|
|
* - Ratio d'aspect personnalisable
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* // Recadrage d'avatar (carré, circulaire)
|
|
|
|
|
* <ImageCropper
|
|
|
|
|
* imageSrc={avatarSrc}
|
|
|
|
|
* aspectRatio={1}
|
|
|
|
|
* circularCrop={true}
|
|
|
|
|
* onCancel={() => setShowCropper(false)}
|
|
|
|
|
* onCropComplete={(area) => handleCrop(area)}
|
|
|
|
|
* />
|
|
|
|
|
* ```
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* // Recadrage de bannière (paysage, rectangulaire)
|
|
|
|
|
* <ImageCropper
|
|
|
|
|
* imageSrc={bannerSrc}
|
|
|
|
|
* aspectRatio={3}
|
|
|
|
|
* circularCrop={false}
|
|
|
|
|
* onCancel={() => setShowCropper(false)}
|
|
|
|
|
* onCropComplete={(area) => handleCrop(area)}
|
|
|
|
|
* />
|
|
|
|
|
* ```
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2026-01-07 09:31:02 +00:00
|
|
|
* @component
|
|
|
|
|
* @param {ImageCropperProps} props - Propriétés du composant
|
|
|
|
|
* @returns {JSX.Element} Modal de recadrage avec contrôles
|
|
|
|
|
*/
|
|
|
|
|
|
2026-01-07 18:39:21 +00:00
|
|
|
export const ImageCropper: React.FC<ImageCropperProps> = ({
|
|
|
|
|
imageSrc,
|
|
|
|
|
aspectRatio,
|
|
|
|
|
onCancel,
|
|
|
|
|
onCropComplete,
|
2026-01-13 18:47:57 +00:00
|
|
|
circularCrop = false,
|
2026-01-07 09:31:02 +00:00
|
|
|
}) => {
|
|
|
|
|
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);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const onCropCompleteHandler = useCallback(
|
|
|
|
|
(_croppedArea: any, croppedAreaPixels: any) => {
|
|
|
|
|
setCroppedAreaPixels(croppedAreaPixels);
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
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">
|
ui(tokens): migrate border-kodo-steel to border-border (86 files, 269 instances)
Replace legacy hardcoded border-kodo-steel (RGB 59,69,84, theme-unaware)
with semantic border-border token across 86 user-facing components.
Covers UI primitives (checkbox, badge, modal, table, textarea, alert,
radio-group, avatar), all modals, settings views, social features,
playlist views, inventory, chat, commerce, and cloud file browser.
Only story/test files retain the legacy token.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:07:00 +00:00
|
|
|
<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]">
|
2026-01-07 09:31:02 +00:00
|
|
|
{/* Header */}
|
ui(tokens): migrate border-kodo-steel to border-border (86 files, 269 instances)
Replace legacy hardcoded border-kodo-steel (RGB 59,69,84, theme-unaware)
with semantic border-border token across 86 user-facing components.
Covers UI primitives (checkbox, badge, modal, table, textarea, alert,
radio-group, avatar), all modals, settings views, social features,
playlist views, inventory, chat, commerce, and cloud file browser.
Only story/test files retain the legacy token.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:07:00 +00:00
|
|
|
<div className="p-4 border-b border-border flex justify-between items-center bg-kodo-ink">
|
2026-01-07 09:31:02 +00:00
|
|
|
<h3 className="font-bold text-white flex items-center gap-2">
|
|
|
|
|
Edit Image
|
|
|
|
|
</h3>
|
consistency: replace custom buttons with Button component (partial)
- Replaced custom button implementations with Button component in 14 files
- Files updated: LiveStreamDetailView, DashboardPage, CommentItem, PostCard, SocialPage, SocialView, AdminUsersView, UserTableRow, ProjectsManager, CloudFileBrowser, FileManagerView, CreatorModal, ImageCropper, BulkUploadModal
- ~31 buttons replaced across high-priority files
- Used appropriate Button variants: ghost, outline, default, secondary, link
- Preserved visual appearance with className overrides where needed
- Action 9.2.1.2 in progress (partial completion)
2026-01-16 01:06:14 +00:00
|
|
|
<Button variant="ghost" size="icon" onClick={onCancel}>
|
2026-01-13 18:47:57 +00:00
|
|
|
<X className="w-5 h-5" />
|
consistency: replace custom buttons with Button component (partial)
- Replaced custom button implementations with Button component in 14 files
- Files updated: LiveStreamDetailView, DashboardPage, CommentItem, PostCard, SocialPage, SocialView, AdminUsersView, UserTableRow, ProjectsManager, CloudFileBrowser, FileManagerView, CreatorModal, ImageCropper, BulkUploadModal
- ~31 buttons replaced across high-priority files
- Used appropriate Button variants: ghost, outline, default, secondary, link
- Preserved visual appearance with className overrides where needed
- Action 9.2.1.2 in progress (partial completion)
2026-01-16 01:06:14 +00:00
|
|
|
</Button>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Cropper Area */}
|
|
|
|
|
<div className="relative flex-1 bg-black">
|
2026-01-13 18:47:57 +00:00
|
|
|
<Suspense
|
|
|
|
|
fallback={
|
|
|
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
|
|
|
<LoadingSpinner size="lg" text="Chargement du recadreur..." />
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
>
|
2026-01-07 09:31:02 +00:00
|
|
|
<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 */}
|
ui(tokens): migrate border-kodo-steel to border-border (86 files, 269 instances)
Replace legacy hardcoded border-kodo-steel (RGB 59,69,84, theme-unaware)
with semantic border-border token across 86 user-facing components.
Covers UI primitives (checkbox, badge, modal, table, textarea, alert,
radio-group, avatar), all modals, settings views, social features,
playlist views, inventory, chat, commerce, and cloud file browser.
Only story/test files retain the legacy token.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:07:00 +00:00
|
|
|
<div className="p-8 bg-kodo-ink border-t border-border space-y-4">
|
2026-01-07 18:39:21 +00:00
|
|
|
<div className="flex items-center gap-4">
|
2026-02-08 23:04:51 +00:00
|
|
|
<span className="text-xs text-muted-foreground w-16">Zoom</span>
|
|
|
|
|
<ZoomIn className="w-4 h-4 text-muted-foreground" />
|
2026-01-07 18:39:21 +00:00
|
|
|
<input
|
|
|
|
|
type="range"
|
|
|
|
|
value={zoom}
|
|
|
|
|
min={1}
|
|
|
|
|
max={3}
|
|
|
|
|
step={0.1}
|
|
|
|
|
aria-labelledby="Zoom"
|
|
|
|
|
onChange={(e) => setZoom(Number(e.target.value))}
|
ui(tokens): migrate kodo-cyan to primary (51 files, 88 instances)
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>
2026-02-08 23:19:12 +00:00
|
|
|
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"
|
2026-01-07 18:39:21 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-4">
|
2026-02-08 23:04:51 +00:00
|
|
|
<span className="text-xs text-muted-foreground w-16">Rotate</span>
|
|
|
|
|
<RotateCw className="w-4 h-4 text-muted-foreground" />
|
2026-01-07 18:39:21 +00:00
|
|
|
<input
|
|
|
|
|
type="range"
|
|
|
|
|
value={rotation}
|
|
|
|
|
min={0}
|
|
|
|
|
max={360}
|
|
|
|
|
step={1}
|
|
|
|
|
aria-labelledby="Rotation"
|
|
|
|
|
onChange={(e) => setRotation(Number(e.target.value))}
|
ui(tokens): migrate kodo-cyan to primary (51 files, 88 instances)
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>
2026-02-08 23:19:12 +00:00
|
|
|
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"
|
2026-01-07 18:39:21 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 10:50:46 +00:00
|
|
|
<div className="flex justify-end gap-4 pt-2">
|
2026-01-13 18:47:57 +00:00
|
|
|
<Button variant="ghost" onClick={onCancel}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="primary"
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
icon={<Check className="w-4 h-4" />}
|
|
|
|
|
>
|
|
|
|
|
Apply Crop
|
|
|
|
|
</Button>
|
2026-01-07 18:39:21 +00:00
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-01-13 18:47:57 +00:00
|
|
|
};
|