From 687d02ff3abe80712d0f6b4b8724836918637e3a Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 28 Sep 2025 22:10:12 +0200 Subject: [PATCH] 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. --- src/components/kanban/Board.tsx | 2 +- src/components/kanban/Column.tsx | 70 +++---- src/components/kanban/SwimlanesBase.tsx | 28 ++- .../ui-showcase/UIShowcaseClient.tsx | 106 ++++++++++- src/components/ui/Card.tsx | 172 ++++++++++++++---- src/components/ui/ColumnHeader.tsx | 77 ++++++++ src/components/ui/DropZone.tsx | 31 ++++ src/components/ui/EmptyState.tsx | 107 +++++++++++ src/components/ui/index.ts | 3 + 9 files changed, 499 insertions(+), 97 deletions(-) create mode 100644 src/components/ui/ColumnHeader.tsx create mode 100644 src/components/ui/DropZone.tsx create mode 100644 src/components/ui/EmptyState.tsx diff --git a/src/components/kanban/Board.tsx b/src/components/kanban/Board.tsx index e2468a1..c058cb5 100644 --- a/src/components/kanban/Board.tsx +++ b/src/components/kanban/Board.tsx @@ -86,7 +86,7 @@ export function KanbanBoard({ tasks, onCreateTask, onEditTask, onUpdateStatus, c
{/* Board tech dark */} -
+
{visibleColumns.map((column) => ( - - -
-
-
-

- {statusConfig.label} {statusConfig.icon} -

-
-
- - {String(tasks.length).padStart(2, '0')} - - {onCreateTask && ( - - )} -
-
-
+
+ + + + setShowQuickAdd(true)} + /> +
@@ -78,15 +59,11 @@ export function KanbanColumn({ id, tasks, onCreateTask, onEditTask, compactView )} {tasks.length === 0 && !showQuickAdd ? ( -
-
- {statusConfig.icon} -
-

NO DATA

-
-
-
-
+ ) : ( tasks.map((task) => ( @@ -94,7 +71,8 @@ export function KanbanColumn({ id, tasks, onCreateTask, onEditTask, compactView )}
-
+ +
); } diff --git a/src/components/kanban/SwimlanesBase.tsx b/src/components/kanban/SwimlanesBase.tsx index 0057f9d..176058d 100644 --- a/src/components/kanban/SwimlanesBase.tsx +++ b/src/components/kanban/SwimlanesBase.tsx @@ -8,6 +8,7 @@ import { useState } from 'react'; import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useDragAndDrop } from '@/hooks/useDragAndDrop'; import { getAllStatuses, getTechStyle } from '@/lib/status-config'; +import { Card, CardHeader, ColumnHeader, DropZone } from '@/components/ui'; import { DndContext, DragEndEvent, @@ -49,7 +50,7 @@ function DroppableColumn({ }); return ( -
+ t.id)} strategy={verticalListSortingStrategy}>
{tasks.map(task => ( @@ -90,7 +91,7 @@ function DroppableColumn({ )} )} -
+
); } @@ -197,11 +198,18 @@ export function SwimlanesBase({ {statusesToShow.map(status => { const statusConfig = allStatuses.find(s => s.key === status); const techStyle = statusConfig ? getTechStyle(statusConfig.color) : null; + const tasksInStatus = tasks.filter(task => task.status === status); + return (
-

- {statusConfig?.icon} {statusConfig?.label} -

+
); })} @@ -214,12 +222,12 @@ export function SwimlanesBase({ const isCollapsed = collapsedSwimlanes.has(swimlane.key); return ( -
+ {/* Header de la swimlane */} -
+ -
+ {/* Contenu de la swimlane */} {!isCollapsed && ( @@ -272,7 +280,7 @@ export function SwimlanesBase({ })}
)} -
+
); })}
diff --git a/src/components/ui-showcase/UIShowcaseClient.tsx b/src/components/ui-showcase/UIShowcaseClient.tsx index f56c5ad..db206f2 100644 --- a/src/components/ui-showcase/UIShowcaseClient.tsx +++ b/src/components/ui-showcase/UIShowcaseClient.tsx @@ -8,7 +8,7 @@ import { Alert, AlertTitle, AlertDescription } from '@/components/ui/Alert'; import { Input } from '@/components/ui/Input'; import { StyledCard } from '@/components/ui/StyledCard'; 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'; export function UIShowcaseClient() { @@ -244,6 +244,54 @@ export function UIShowcaseClient() {

+ + {/* Nouveaux exemples avec props flexibles */} +
+

Card Flexible Props

+
+
+
+ shadow="lg" border="primary" +
+ + + Small Header + + +

Small padding content.

+
+
+
+ +
+
+ background="column" shadow="md" +
+ + + No Separator + + +

Large padding, no separator.

+
+
+
+ +
+
+ border="none" shadow="none" +
+ + + Minimal Card + + +

No border, no shadow, no padding.

+
+
+
+
+
{/* Interactive Demo */} @@ -797,6 +845,62 @@ export function UIShowcaseClient() { + + {/* Column Components */} +
+

Column Components

+
+
+
+ ColumnHeader - Header de colonne Kanban +
+
+ console.log('Add task')} + /> +
+
+ +
+
+ EmptyState - État vide avec icône +
+
+ +
+
+ +
+
+ DropZone - Zone de drop avec animation +
+
+ +
+

Zone de drop normale

+
+
+ +
+

Zone de drop active (isOver=true)

+
+
+
+
+
+
{/* Footer */} diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index 7790b4f..5f5e634 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -3,23 +3,54 @@ import { cn } from '@/lib/utils'; interface CardProps extends HTMLAttributes { variant?: 'default' | 'elevated' | 'bordered' | 'column'; + shadow?: 'none' | 'sm' | 'md' | 'lg'; + border?: 'none' | 'default' | 'primary' | 'accent'; + background?: 'default' | 'column' | 'muted'; } const Card = forwardRef( - ({ 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 (
( Card.displayName = 'Card'; -const CardHeader = forwardRef>( - ({ className, ...props }, ref) => ( -
- ) +interface CardHeaderProps extends HTMLAttributes { + separator?: boolean; + padding?: 'sm' | 'md' | 'lg'; +} + +const CardHeader = forwardRef( + ({ className, separator = true, padding = 'md', ...props }, ref) => { + const paddings = { + sm: 'p-2', + md: 'p-4', + lg: 'p-6' + }; + + return ( +
+ ); + } ); CardHeader.displayName = 'CardHeader'; -const CardTitle = forwardRef>( - ({ className, ...props }, ref) => ( -

- ) +interface CardTitleProps extends HTMLAttributes { + size?: 'sm' | 'md' | 'lg'; +} + +const CardTitle = forwardRef( + ({ className, size = 'md', ...props }, ref) => { + const sizes = { + sm: 'text-sm', + md: 'text-base', + lg: 'text-lg' + }; + + return ( +

+ ); + } ); CardTitle.displayName = 'CardTitle'; -const CardContent = forwardRef>( - ({ className, ...props }, ref) => ( -
- ) +interface CardContentProps extends HTMLAttributes { + padding?: 'sm' | 'md' | 'lg' | 'none'; +} + +const CardContent = forwardRef( + ({ className, padding = 'md', ...props }, ref) => { + const paddings = { + none: '', + sm: 'p-2', + md: 'p-4', + lg: 'p-6' + }; + + return ( +
+ ); + } ); CardContent.displayName = 'CardContent'; -const CardFooter = forwardRef>( - ({ className, ...props }, ref) => ( -
- ) +interface CardFooterProps extends HTMLAttributes { + separator?: boolean; + padding?: 'sm' | 'md' | 'lg'; +} + +const CardFooter = forwardRef( + ({ className, separator = true, padding = 'md', ...props }, ref) => { + const paddings = { + sm: 'p-2', + md: 'p-4', + lg: 'p-6' + }; + + return ( +
+ ); + } ); CardFooter.displayName = 'CardFooter'; diff --git a/src/components/ui/ColumnHeader.tsx b/src/components/ui/ColumnHeader.tsx new file mode 100644 index 0000000..a16c46e --- /dev/null +++ b/src/components/ui/ColumnHeader.tsx @@ -0,0 +1,77 @@ +import { HTMLAttributes, forwardRef } from 'react'; +import { cn } from '@/lib/utils'; +import { Badge } from './Badge'; + +interface ColumnHeaderProps extends HTMLAttributes { + title: string; + icon?: string; + count: number; + color?: string; + accentColor?: string; + borderColor?: string; + onAddClick?: () => void; + showAddButton?: boolean; +} + +const ColumnHeader = forwardRef( + ({ + className, + title, + icon, + count, + color, + accentColor, + borderColor, + onAddClick, + showAddButton = false, + ...props + }, ref) => { + return ( +
+
+
+

+ {title} {icon} +

+
+ +
+ + {String(count).padStart(2, '0')} + + {showAddButton && onAddClick && ( + + )} +
+
+ ); + } +); + +ColumnHeader.displayName = 'ColumnHeader'; + +export { ColumnHeader }; diff --git a/src/components/ui/DropZone.tsx b/src/components/ui/DropZone.tsx new file mode 100644 index 0000000..7956aac --- /dev/null +++ b/src/components/ui/DropZone.tsx @@ -0,0 +1,31 @@ +import { HTMLAttributes, forwardRef } from 'react'; +import { cn } from '@/lib/utils'; + +interface DropZoneProps extends HTMLAttributes { + isOver?: boolean; + children: React.ReactNode; +} + +const DropZone = forwardRef( + ({ className, isOver = false, children, ...props }, ref) => { + return ( +
+ {children} +
+ ); + } +); + +DropZone.displayName = 'DropZone'; + +export { DropZone }; diff --git a/src/components/ui/EmptyState.tsx b/src/components/ui/EmptyState.tsx new file mode 100644 index 0000000..aca3dfb --- /dev/null +++ b/src/components/ui/EmptyState.tsx @@ -0,0 +1,107 @@ +import { HTMLAttributes, forwardRef } from 'react'; +import { cn } from '@/lib/utils'; + +interface EmptyStateProps extends HTMLAttributes { + icon?: string; + title?: string; + description?: string; + accentColor?: string; + borderColor?: string; + size?: 'sm' | 'md' | 'lg'; +} + +const EmptyState = forwardRef( + ({ + 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 ( +
+
+ + {icon || "📋"} + +
+ +

+ {title} +

+ + {description && ( +

+ {description} +

+ )} + +
+
+
+
+ ); + } +); + +EmptyState.displayName = 'EmptyState'; + +export { EmptyState }; diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index b69b5b4..ab4b8a6 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -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';