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:
@@ -3,23 +3,54 @@ import { cn } from '@/lib/utils';
|
||||
|
||||
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
variant?: 'default' | 'elevated' | 'bordered' | 'column';
|
||||
shadow?: 'none' | 'sm' | 'md' | 'lg';
|
||||
border?: 'none' | 'default' | 'primary' | 'accent';
|
||||
background?: 'default' | 'column' | 'muted';
|
||||
}
|
||||
|
||||
const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
({ className, variant = 'default', ...props }, ref) => {
|
||||
const variants = {
|
||||
default: 'bg-[var(--card)]/50 border border-[var(--border)]/50',
|
||||
elevated: 'bg-[var(--card)]/80 border border-[var(--border)]/50 shadow-lg shadow-[var(--card)]/20',
|
||||
bordered: 'bg-[var(--card)]/50 border border-[var(--primary)]/30 shadow-[var(--primary)]/10 shadow-lg',
|
||||
column: 'bg-[var(--card-column)] border border-[var(--border)]/50 shadow-lg shadow-[var(--card)]/20'
|
||||
({ className, variant = 'default', shadow = 'sm', border = 'default', background = 'default', ...props }, ref) => {
|
||||
const backgrounds = {
|
||||
default: 'bg-[var(--card)]',
|
||||
column: 'bg-[var(--card-column)]',
|
||||
muted: 'bg-[var(--muted)]/10'
|
||||
};
|
||||
|
||||
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 (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg backdrop-blur-sm transition-all duration-200',
|
||||
variants[variant],
|
||||
finalBackground,
|
||||
finalBorder,
|
||||
finalShadow,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -30,50 +61,113 @@ const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('p-4 border-b border-[var(--border)]/50', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
interface CardHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
||||
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
|
||||
ref={ref}
|
||||
className={cn(
|
||||
paddings[padding],
|
||||
separator && 'border-b border-[var(--border)]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn('font-mono font-semibold text-[var(--foreground)] tracking-wide', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {
|
||||
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
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'font-mono font-semibold text-[var(--foreground)] tracking-wide',
|
||||
sizes[size],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
interface CardContentProps extends HTMLAttributes<HTMLDivElement> {
|
||||
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
|
||||
ref={ref}
|
||||
className={cn(paddings[padding], className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('p-4 border-t border-[var(--border)]/50', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
interface CardFooterProps extends HTMLAttributes<HTMLDivElement> {
|
||||
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
|
||||
ref={ref}
|
||||
className={cn(
|
||||
paddings[padding],
|
||||
separator && 'border-t border-[var(--border)]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
77
src/components/ui/ColumnHeader.tsx
Normal file
77
src/components/ui/ColumnHeader.tsx
Normal 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 };
|
||||
31
src/components/ui/DropZone.tsx
Normal file
31
src/components/ui/DropZone.tsx
Normal 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 };
|
||||
107
src/components/ui/EmptyState.tsx
Normal file
107
src/components/ui/EmptyState.tsx
Normal 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 };
|
||||
@@ -18,6 +18,9 @@ export { SearchInput } from './SearchInput';
|
||||
export { ControlPanel, ControlSection, ControlGroup } from './ControlPanel';
|
||||
export { FilterSummary } from './FilterSummary';
|
||||
export { FilterChip } from './FilterChip';
|
||||
export { ColumnHeader } from './ColumnHeader';
|
||||
export { EmptyState } from './EmptyState';
|
||||
export { DropZone } from './DropZone';
|
||||
|
||||
// Composants existants
|
||||
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
|
||||
|
||||
Reference in New Issue
Block a user