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:
@@ -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}
|
||||||
|
|||||||
@@ -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,41 +26,23 @@ 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"
|
<CardHeader className="pb-4">
|
||||||
className={`h-full flex flex-col transition-all duration-200 ${
|
<ColumnHeader
|
||||||
isOver ? 'ring-2 ring-[var(--primary)]/50 bg-[var(--card-hover)]' : ''
|
title={statusConfig.label}
|
||||||
}`}
|
icon={statusConfig.icon}
|
||||||
>
|
count={tasks.length}
|
||||||
<CardHeader className="pb-4">
|
color={style.accent.replace('text-', '')}
|
||||||
<div className="flex items-center justify-between">
|
accentColor={style.accent}
|
||||||
<div className="flex items-center gap-3">
|
borderColor={style.border}
|
||||||
<div className={`w-2 h-2 rounded-full ${style.accent.replace('text-', 'bg-')} animate-pulse`}></div>
|
showAddButton={!!onCreateTask}
|
||||||
<h3 className={`font-mono text-sm font-bold ${style.accent} uppercase tracking-wider`}>
|
onAddClick={() => setShowQuickAdd(true)}
|
||||||
{statusConfig.label} {statusConfig.icon}
|
/>
|
||||||
</h3>
|
</CardHeader>
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant={badgeVariant} size="sm">
|
|
||||||
{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>
|
|
||||||
|
|
||||||
<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">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -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} />
|
||||||
@@ -94,7 +71,8 @@ export function KanbanColumn({ id, tasks, onCreateTask, onEditTask, compactView
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</DropZone>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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;
|
||||||
<div
|
padding?: 'sm' | 'md' | 'lg';
|
||||||
ref={ref}
|
}
|
||||||
className={cn('p-4 border-b border-[var(--border)]/50', className)}
|
|
||||||
{...props}
|
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';
|
CardHeader.displayName = 'CardHeader';
|
||||||
|
|
||||||
const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
|
interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {
|
||||||
({ className, ...props }, ref) => (
|
size?: 'sm' | 'md' | 'lg';
|
||||||
<h3
|
}
|
||||||
ref={ref}
|
|
||||||
className={cn('font-mono font-semibold text-[var(--foreground)] tracking-wide', className)}
|
const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(
|
||||||
{...props}
|
({ 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';
|
CardTitle.displayName = 'CardTitle';
|
||||||
|
|
||||||
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
interface CardContentProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
({ className, ...props }, ref) => (
|
padding?: 'sm' | 'md' | 'lg' | 'none';
|
||||||
<div
|
}
|
||||||
ref={ref}
|
|
||||||
className={cn('p-4', className)}
|
const CardContent = forwardRef<HTMLDivElement, CardContentProps>(
|
||||||
{...props}
|
({ 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';
|
CardContent.displayName = 'CardContent';
|
||||||
|
|
||||||
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
interface CardFooterProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
({ className, ...props }, ref) => (
|
separator?: boolean;
|
||||||
<div
|
padding?: 'sm' | 'md' | 'lg';
|
||||||
ref={ref}
|
}
|
||||||
className={cn('p-4 border-t border-[var(--border)]/50', className)}
|
|
||||||
{...props}
|
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';
|
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 { 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';
|
||||||
|
|||||||
Reference in New Issue
Block a user