feat: enhance Kanban components with new UI elements

- Added `ColumnHeader`, `EmptyState`, and `DropZone` components to improve the Kanban UI structure and user experience.
- Refactored `KanbanColumn` to utilize the new components, enhancing readability and maintainability.
- Updated `Card` component to support flexible props for shadow, border, and background, allowing for better customization across the application.
- Adjusted `SwimlanesBase` to incorporate the new `ColumnHeader` for consistent column representation.
This commit is contained in:
Julien Froidefond
2025-09-28 22:10:12 +02:00
parent 5a3d825b8e
commit 687d02ff3a
9 changed files with 499 additions and 97 deletions

View File

@@ -86,7 +86,7 @@ export function KanbanBoard({ tasks, onCreateTask, onEditTask, onUpdateStatus, c
<div className="pt-4"></div> <div className="pt-4"></div>
{/* Board tech dark */} {/* Board tech dark */}
<div className="flex-1 flex gap-3 overflow-x-auto p-6"> <div className="flex-1 flex gap-3 overflow-x-auto p-6 min-w-0">
{visibleColumns.map((column) => ( {visibleColumns.map((column) => (
<KanbanColumn <KanbanColumn
key={column.id} key={column.id}

View File

@@ -1,12 +1,11 @@
import { Task, TaskStatus } from '@/lib/types'; import { Task, TaskStatus } from '@/lib/types';
import { TaskCard } from './TaskCard'; import { TaskCard } from './TaskCard';
import { QuickAddTask } from './QuickAddTask'; import { QuickAddTask } from './QuickAddTask';
import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardContent, ColumnHeader, EmptyState, DropZone } from '@/components/ui';
import { Badge } from '@/components/ui/Badge';
import { CreateTaskData } from '@/clients/tasks-client'; import { CreateTaskData } from '@/clients/tasks-client';
import { useState } from 'react'; import { useState } from 'react';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import { getStatusConfig, getTechStyle, getBadgeVariant } from '@/lib/status-config'; import { getStatusConfig, getTechStyle } from '@/lib/status-config';
interface KanbanColumnProps { interface KanbanColumnProps {
id: TaskStatus; id: TaskStatus;
@@ -27,40 +26,22 @@ export function KanbanColumn({ id, tasks, onCreateTask, onEditTask, compactView
// Récupération de la config du statut // Récupération de la config du statut
const statusConfig = getStatusConfig(id); const statusConfig = getStatusConfig(id);
const style = getTechStyle(statusConfig.color); const style = getTechStyle(statusConfig.color);
const badgeVariant = getBadgeVariant(statusConfig.color);
return ( return (
<div className="flex-shrink-0 w-80 md:w-1/4 md:flex-1 h-full"> <div className="flex-shrink-0 w-72 md:w-72 lg:w-80 h-full">
<Card <DropZone ref={setNodeRef} isOver={isOver}>
ref={setNodeRef} <Card variant="column" className="h-full flex flex-col">
variant="column"
className={`h-full flex flex-col transition-all duration-200 ${
isOver ? 'ring-2 ring-[var(--primary)]/50 bg-[var(--card-hover)]' : ''
}`}
>
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<div className="flex items-center justify-between"> <ColumnHeader
<div className="flex items-center gap-3"> title={statusConfig.label}
<div className={`w-2 h-2 rounded-full ${style.accent.replace('text-', 'bg-')} animate-pulse`}></div> icon={statusConfig.icon}
<h3 className={`font-mono text-sm font-bold ${style.accent} uppercase tracking-wider`}> count={tasks.length}
{statusConfig.label} {statusConfig.icon} color={style.accent.replace('text-', '')}
</h3> accentColor={style.accent}
</div> borderColor={style.border}
<div className="flex items-center gap-2"> showAddButton={!!onCreateTask}
<Badge variant={badgeVariant} size="sm"> onAddClick={() => setShowQuickAdd(true)}
{String(tasks.length).padStart(2, '0')} />
</Badge>
{onCreateTask && (
<button
onClick={() => setShowQuickAdd(true)}
className={`w-5 h-5 rounded-full border border-dashed ${style.border} ${style.accent} hover:bg-[var(--card-hover)] transition-colors flex items-center justify-center text-xs font-mono`}
title="Ajouter une tâche rapide"
>
+
</button>
)}
</div>
</div>
</CardHeader> </CardHeader>
<CardContent className="flex-1 p-4 h-[calc(100vh-220px)] overflow-y-auto"> <CardContent className="flex-1 p-4 h-[calc(100vh-220px)] overflow-y-auto">
@@ -78,15 +59,11 @@ export function KanbanColumn({ id, tasks, onCreateTask, onEditTask, compactView
)} )}
{tasks.length === 0 && !showQuickAdd ? ( {tasks.length === 0 && !showQuickAdd ? (
<div className="text-center py-20"> <EmptyState
<div className={`w-16 h-16 mx-auto mb-4 rounded-full bg-[var(--card)] border-2 border-dashed ${style.border} flex items-center justify-center`}> icon={statusConfig.icon}
<span className={`text-2xl ${style.accent} opacity-50`}>{statusConfig.icon}</span> accentColor={style.accent}
</div> borderColor={style.border}
<p className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide">NO DATA</p> />
<div className="mt-2 flex justify-center">
<div className={`w-8 h-0.5 ${style.accent.replace('text-', 'bg-')} opacity-30`}></div>
</div>
</div>
) : ( ) : (
tasks.map((task) => ( tasks.map((task) => (
<TaskCard key={task.id} task={task} onEdit={onEditTask} compactView={compactView} /> <TaskCard key={task.id} task={task} onEdit={onEditTask} compactView={compactView} />
@@ -95,6 +72,7 @@ export function KanbanColumn({ id, tasks, onCreateTask, onEditTask, compactView
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</DropZone>
</div> </div>
); );
} }

View File

@@ -8,6 +8,7 @@ import { useState } from 'react';
import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useUserPreferences } from '@/contexts/UserPreferencesContext';
import { useDragAndDrop } from '@/hooks/useDragAndDrop'; import { useDragAndDrop } from '@/hooks/useDragAndDrop';
import { getAllStatuses, getTechStyle } from '@/lib/status-config'; import { getAllStatuses, getTechStyle } from '@/lib/status-config';
import { Card, CardHeader, ColumnHeader, DropZone } from '@/components/ui';
import { import {
DndContext, DndContext,
DragEndEvent, DragEndEvent,
@@ -49,7 +50,7 @@ function DroppableColumn({
}); });
return ( return (
<div ref={setNodeRef} className="min-h-[100px] relative group/column"> <DropZone ref={setNodeRef} className="min-h-[100px] relative group/column">
<SortableContext items={tasks.map(t => t.id)} strategy={verticalListSortingStrategy}> <SortableContext items={tasks.map(t => t.id)} strategy={verticalListSortingStrategy}>
<div className="space-y-3"> <div className="space-y-3">
{tasks.map(task => ( {tasks.map(task => (
@@ -90,7 +91,7 @@ function DroppableColumn({
)} )}
</> </>
)} )}
</div> </DropZone>
); );
} }
@@ -197,11 +198,18 @@ export function SwimlanesBase({
{statusesToShow.map(status => { {statusesToShow.map(status => {
const statusConfig = allStatuses.find(s => s.key === status); const statusConfig = allStatuses.find(s => s.key === status);
const techStyle = statusConfig ? getTechStyle(statusConfig.color) : null; const techStyle = statusConfig ? getTechStyle(statusConfig.color) : null;
const tasksInStatus = tasks.filter(task => task.status === status);
return ( return (
<div key={status} className="text-center"> <div key={status} className="text-center">
<h3 className={`text-sm font-mono font-bold uppercase tracking-wider ${techStyle?.accent || 'text-[var(--foreground)]'}`}> <ColumnHeader
{statusConfig?.icon} {statusConfig?.label} title={statusConfig?.label || status}
</h3> icon={statusConfig?.icon}
count={tasksInStatus.length}
color={techStyle?.accent.replace('text-', '')}
accentColor={techStyle?.accent}
className="justify-center gap-4"
/>
</div> </div>
); );
})} })}
@@ -214,12 +222,12 @@ export function SwimlanesBase({
const isCollapsed = collapsedSwimlanes.has(swimlane.key); const isCollapsed = collapsedSwimlanes.has(swimlane.key);
return ( return (
<div key={swimlane.key} className="border border-[var(--border)]/50 rounded-lg bg-[var(--card-column)]"> <Card key={swimlane.key} background="column" className="overflow-hidden">
{/* Header de la swimlane */} {/* Header de la swimlane */}
<div className="flex items-center p-2 border-b border-[var(--border)]/50"> <CardHeader padding="sm" separator={false}>
<button <button
onClick={() => toggleSwimlane(swimlane.key)} onClick={() => toggleSwimlane(swimlane.key)}
className="flex items-center gap-2 hover:bg-[var(--card-hover)] rounded p-1 -m-1 transition-colors" className="flex items-center gap-2 hover:bg-[var(--card-hover)] rounded p-1 -m-1 transition-colors w-full"
> >
<svg <svg
className={`w-4 h-4 text-[var(--muted-foreground)] transition-transform ${isCollapsed ? '' : 'rotate-90'}`} className={`w-4 h-4 text-[var(--muted-foreground)] transition-transform ${isCollapsed ? '' : 'rotate-90'}`}
@@ -240,7 +248,7 @@ export function SwimlanesBase({
{swimlane.label} ({swimlane.tasks.length}) {swimlane.label} ({swimlane.tasks.length})
</span> </span>
</button> </button>
</div> </CardHeader>
{/* Contenu de la swimlane */} {/* Contenu de la swimlane */}
{!isCollapsed && ( {!isCollapsed && (
@@ -272,7 +280,7 @@ export function SwimlanesBase({
})} })}
</div> </div>
)} )}
</div> </Card>
); );
})} })}
</div> </div>

View File

@@ -8,7 +8,7 @@ import { Alert, AlertTitle, AlertDescription } from '@/components/ui/Alert';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { StyledCard } from '@/components/ui/StyledCard'; import { StyledCard } from '@/components/ui/StyledCard';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { StatCard, ProgressBar, ActionCard, TaskCard, MetricCard, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup, FilterSummary, FilterChip } from '@/components/ui'; import { StatCard, ProgressBar, ActionCard, TaskCard, MetricCard, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup, FilterSummary, FilterChip, ColumnHeader, EmptyState, DropZone } from '@/components/ui';
import { ThemeSelector } from '@/components/ThemeSelector'; import { ThemeSelector } from '@/components/ThemeSelector';
export function UIShowcaseClient() { export function UIShowcaseClient() {
@@ -244,6 +244,54 @@ export function UIShowcaseClient() {
</p> </p>
</StyledCard> </StyledCard>
</div> </div>
{/* Nouveaux exemples avec props flexibles */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">Card Flexible Props</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
shadow="lg" border="primary"
</div>
<Card shadow="lg" border="primary">
<CardHeader padding="sm">
<CardTitle size="sm">Small Header</CardTitle>
</CardHeader>
<CardContent padding="sm">
<p className="text-sm text-[var(--muted-foreground)]">Small padding content.</p>
</CardContent>
</Card>
</div>
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
background="column" shadow="md"
</div>
<Card background="column" shadow="md">
<CardHeader separator={false}>
<CardTitle size="lg">No Separator</CardTitle>
</CardHeader>
<CardContent padding="lg">
<p className="text-sm text-[var(--muted-foreground)]">Large padding, no separator.</p>
</CardContent>
</Card>
</div>
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
border="none" shadow="none"
</div>
<Card border="none" shadow="none">
<CardHeader padding="md">
<CardTitle>Minimal Card</CardTitle>
</CardHeader>
<CardContent padding="none">
<p className="text-sm text-[var(--muted-foreground)]">No border, no shadow, no padding.</p>
</CardContent>
</Card>
</div>
</div>
</div>
</section> </section>
{/* Interactive Demo */} {/* Interactive Demo */}
@@ -797,6 +845,62 @@ export function UIShowcaseClient() {
</div> </div>
</div> </div>
</div> </div>
{/* Column Components */}
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">Column Components</h3>
<div className="space-y-4">
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
ColumnHeader - Header de colonne Kanban
</div>
<div className="max-w-md">
<ColumnHeader
title="En cours"
icon="🔄"
count={5}
color="blue"
accentColor="text-blue-500"
borderColor="border-blue-300"
showAddButton={true}
onAddClick={() => console.log('Add task')}
/>
</div>
</div>
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
EmptyState - État vide avec icône
</div>
<div className="max-w-md">
<EmptyState
icon="📋"
title="NO DATA"
accentColor="text-gray-500"
borderColor="border-gray-300"
/>
</div>
</div>
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
DropZone - Zone de drop avec animation
</div>
<div className="max-w-md">
<DropZone isOver={false}>
<div className="p-4 border border-[var(--border)] rounded-lg bg-[var(--card)]">
<p className="text-sm text-[var(--muted-foreground)]">Zone de drop normale</p>
</div>
</DropZone>
<DropZone isOver={true}>
<div className="p-4 border border-[var(--border)] rounded-lg bg-[var(--card)]">
<p className="text-sm text-[var(--muted-foreground)]">Zone de drop active (isOver=true)</p>
</div>
</DropZone>
</div>
</div>
</div>
</div>
</section> </section>
{/* Footer */} {/* Footer */}

View File

@@ -3,23 +3,54 @@ import { cn } from '@/lib/utils';
interface CardProps extends HTMLAttributes<HTMLDivElement> { interface CardProps extends HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'elevated' | 'bordered' | 'column'; variant?: 'default' | 'elevated' | 'bordered' | 'column';
shadow?: 'none' | 'sm' | 'md' | 'lg';
border?: 'none' | 'default' | 'primary' | 'accent';
background?: 'default' | 'column' | 'muted';
} }
const Card = forwardRef<HTMLDivElement, CardProps>( const Card = forwardRef<HTMLDivElement, CardProps>(
({ className, variant = 'default', ...props }, ref) => { ({ className, variant = 'default', shadow = 'sm', border = 'default', background = 'default', ...props }, ref) => {
const variants = { const backgrounds = {
default: 'bg-[var(--card)]/50 border border-[var(--border)]/50', default: 'bg-[var(--card)]',
elevated: 'bg-[var(--card)]/80 border border-[var(--border)]/50 shadow-lg shadow-[var(--card)]/20', column: 'bg-[var(--card-column)]',
bordered: 'bg-[var(--card)]/50 border border-[var(--primary)]/30 shadow-[var(--primary)]/10 shadow-lg', muted: 'bg-[var(--muted)]/10'
column: 'bg-[var(--card-column)] border border-[var(--border)]/50 shadow-lg shadow-[var(--card)]/20'
}; };
const borders = {
none: '',
default: 'border border-[var(--border)]',
primary: 'border border-[var(--primary)]/30',
accent: 'border border-[var(--accent)]/30'
};
const shadows = {
none: '',
sm: 'shadow-sm',
md: 'shadow-md',
lg: 'shadow-lg'
};
// Variants prédéfinis pour la rétrocompatibilité
const variantStyles = {
default: '',
elevated: 'shadow-lg',
bordered: 'border-[var(--primary)]/30 shadow-lg',
column: 'bg-[var(--card-column)] shadow-lg'
};
// Appliquer le variant si spécifié, sinon utiliser les props individuelles
const finalShadow = variant !== 'default' ? variantStyles[variant] : shadows[shadow];
const finalBorder = variant !== 'default' ? variantStyles[variant] : borders[border];
const finalBackground = variant !== 'default' ? variantStyles[variant] : backgrounds[background];
return ( return (
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
'rounded-lg backdrop-blur-sm transition-all duration-200', 'rounded-lg backdrop-blur-sm transition-all duration-200',
variants[variant], finalBackground,
finalBorder,
finalShadow,
className className
)} )}
{...props} {...props}
@@ -30,50 +61,113 @@ const Card = forwardRef<HTMLDivElement, CardProps>(
Card.displayName = 'Card'; Card.displayName = 'Card';
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>( interface CardHeaderProps extends HTMLAttributes<HTMLDivElement> {
({ className, ...props }, ref) => ( separator?: boolean;
padding?: 'sm' | 'md' | 'lg';
}
const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
({ className, separator = true, padding = 'md', ...props }, ref) => {
const paddings = {
sm: 'p-2',
md: 'p-4',
lg: 'p-6'
};
return (
<div <div
ref={ref} ref={ref}
className={cn('p-4 border-b border-[var(--border)]/50', className)} className={cn(
paddings[padding],
separator && 'border-b border-[var(--border)]',
className
)}
{...props} {...props}
/> />
) );
}
); );
CardHeader.displayName = 'CardHeader'; CardHeader.displayName = 'CardHeader';
const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>( interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {
({ className, ...props }, ref) => ( size?: 'sm' | 'md' | 'lg';
}
const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(
({ className, size = 'md', ...props }, ref) => {
const sizes = {
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg'
};
return (
<h3 <h3
ref={ref} ref={ref}
className={cn('font-mono font-semibold text-[var(--foreground)] tracking-wide', className)} className={cn(
'font-mono font-semibold text-[var(--foreground)] tracking-wide',
sizes[size],
className
)}
{...props} {...props}
/> />
) );
}
); );
CardTitle.displayName = 'CardTitle'; CardTitle.displayName = 'CardTitle';
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>( interface CardContentProps extends HTMLAttributes<HTMLDivElement> {
({ className, ...props }, ref) => ( padding?: 'sm' | 'md' | 'lg' | 'none';
}
const CardContent = forwardRef<HTMLDivElement, CardContentProps>(
({ className, padding = 'md', ...props }, ref) => {
const paddings = {
none: '',
sm: 'p-2',
md: 'p-4',
lg: 'p-6'
};
return (
<div <div
ref={ref} ref={ref}
className={cn('p-4', className)} className={cn(paddings[padding], className)}
{...props} {...props}
/> />
) );
}
); );
CardContent.displayName = 'CardContent'; CardContent.displayName = 'CardContent';
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>( interface CardFooterProps extends HTMLAttributes<HTMLDivElement> {
({ className, ...props }, ref) => ( separator?: boolean;
padding?: 'sm' | 'md' | 'lg';
}
const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
({ className, separator = true, padding = 'md', ...props }, ref) => {
const paddings = {
sm: 'p-2',
md: 'p-4',
lg: 'p-6'
};
return (
<div <div
ref={ref} ref={ref}
className={cn('p-4 border-t border-[var(--border)]/50', className)} className={cn(
paddings[padding],
separator && 'border-t border-[var(--border)]',
className
)}
{...props} {...props}
/> />
) );
}
); );
CardFooter.displayName = 'CardFooter'; CardFooter.displayName = 'CardFooter';

View File

@@ -0,0 +1,77 @@
import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
import { Badge } from './Badge';
interface ColumnHeaderProps extends HTMLAttributes<HTMLDivElement> {
title: string;
icon?: string;
count: number;
color?: string;
accentColor?: string;
borderColor?: string;
onAddClick?: () => void;
showAddButton?: boolean;
}
const ColumnHeader = forwardRef<HTMLDivElement, ColumnHeaderProps>(
({
className,
title,
icon,
count,
color,
accentColor,
borderColor,
onAddClick,
showAddButton = false,
...props
}, ref) => {
return (
<div
ref={ref}
className={cn("flex items-center justify-between", className)}
{...props}
>
<div className="flex items-center gap-3">
<div
className={cn(
"w-2 h-2 rounded-full animate-pulse",
color ? `bg-${color}` : "bg-[var(--primary)]"
)}
/>
<h3
className={cn(
"font-mono text-sm font-bold uppercase tracking-wider",
accentColor || "text-[var(--foreground)]"
)}
>
{title} {icon}
</h3>
</div>
<div className="flex items-center gap-2">
<Badge variant="default" size="sm">
{String(count).padStart(2, '0')}
</Badge>
{showAddButton && onAddClick && (
<button
onClick={onAddClick}
className={cn(
"w-5 h-5 rounded-full border border-dashed hover:bg-[var(--card-hover)] transition-colors flex items-center justify-center text-xs font-mono",
borderColor || "border-[var(--border)]",
accentColor || "text-[var(--muted-foreground)]"
)}
title="Ajouter une tâche rapide"
>
+
</button>
)}
</div>
</div>
);
}
);
ColumnHeader.displayName = 'ColumnHeader';
export { ColumnHeader };

View File

@@ -0,0 +1,31 @@
import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface DropZoneProps extends HTMLAttributes<HTMLDivElement> {
isOver?: boolean;
children: React.ReactNode;
}
const DropZone = forwardRef<HTMLDivElement, DropZoneProps>(
({ className, isOver = false, children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"transition-all duration-200",
isOver
? "ring-2 ring-[var(--primary)]/50 bg-[var(--card-hover)]"
: "",
className
)}
{...props}
>
{children}
</div>
);
}
);
DropZone.displayName = 'DropZone';
export { DropZone };

View File

@@ -0,0 +1,107 @@
import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface EmptyStateProps extends HTMLAttributes<HTMLDivElement> {
icon?: string;
title?: string;
description?: string;
accentColor?: string;
borderColor?: string;
size?: 'sm' | 'md' | 'lg';
}
const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
({
className,
icon,
title = "NO DATA",
description,
accentColor,
borderColor,
size = 'md',
...props
}, ref) => {
const sizes = {
sm: {
container: 'py-8',
icon: 'w-8 h-8 text-lg',
title: 'text-xs',
divider: 'w-4 h-0.5'
},
md: {
container: 'py-20',
icon: 'w-16 h-16 text-2xl',
title: 'text-xs',
divider: 'w-8 h-0.5'
},
lg: {
container: 'py-32',
icon: 'w-24 h-24 text-3xl',
title: 'text-sm',
divider: 'w-12 h-0.5'
}
};
const currentSize = sizes[size];
return (
<div
ref={ref}
className={cn("text-center", currentSize.container, className)}
{...props}
>
<div
className={cn(
"mx-auto mb-4 rounded-full bg-[var(--card)] border-2 border-dashed flex items-center justify-center",
currentSize.icon,
borderColor || "border-[var(--border)]"
)}
>
<span
className={cn(
"opacity-50",
accentColor || "text-[var(--muted-foreground)]"
)}
>
{icon || "📋"}
</span>
</div>
<p
className={cn(
"font-mono uppercase tracking-wide",
currentSize.title,
accentColor || "text-[var(--muted-foreground)]"
)}
>
{title}
</p>
{description && (
<p
className={cn(
"mt-2 text-xs text-[var(--muted-foreground)]",
accentColor || "text-[var(--muted-foreground)]"
)}
>
{description}
</p>
)}
<div className="mt-2 flex justify-center">
<div
className={cn(
"opacity-30",
currentSize.divider,
accentColor ? accentColor.replace('text-', 'bg-') : "bg-[var(--muted-foreground)]"
)}
/>
</div>
</div>
);
}
);
EmptyState.displayName = 'EmptyState';
export { EmptyState };

View File

@@ -18,6 +18,9 @@ export { SearchInput } from './SearchInput';
export { ControlPanel, ControlSection, ControlGroup } from './ControlPanel'; export { ControlPanel, ControlSection, ControlGroup } from './ControlPanel';
export { FilterSummary } from './FilterSummary'; export { FilterSummary } from './FilterSummary';
export { FilterChip } from './FilterChip'; export { FilterChip } from './FilterChip';
export { ColumnHeader } from './ColumnHeader';
export { EmptyState } from './EmptyState';
export { DropZone } from './DropZone';
// Composants existants // Composants existants
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card'; export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';