feat: add clsx and tailwind-merge dependencies, enhance Kanban components

- Added `clsx` and `tailwind-merge` to `package.json` and `package-lock.json` for improved class management and utility merging.
- Updated `Column` and `TaskCard` components to utilize new UI components (`Card`, `Badge`) for a more cohesive design.
- Refactored styles in `Header` and Kanban components to align with the new design system.
- Marked several tasks as completed in `TODO.md` reflecting progress on UI enhancements.
This commit is contained in:
Julien Froidefond
2025-09-14 08:23:04 +02:00
parent 124e8baee8
commit 79f8035d18
13 changed files with 421 additions and 110 deletions

44
components/ui/Badge.tsx Normal file
View File

@@ -0,0 +1,44 @@
import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'outline';
size?: 'sm' | 'md';
}
const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
({ className, variant = 'default', size = 'md', ...props }, ref) => {
const baseStyles = 'inline-flex items-center font-mono font-medium transition-all duration-200';
const variants = {
default: 'bg-slate-800/50 text-slate-300 border border-slate-600/50',
primary: 'bg-cyan-950/50 text-cyan-300 border border-cyan-500/30',
success: 'bg-emerald-950/50 text-emerald-300 border border-emerald-500/30',
warning: 'bg-yellow-950/50 text-yellow-300 border border-yellow-500/30',
danger: 'bg-red-950/50 text-red-300 border border-red-500/30',
outline: 'bg-transparent text-slate-400 border border-slate-600 hover:bg-slate-800/30 hover:text-slate-300'
};
const sizes = {
sm: 'px-1.5 py-0.5 text-xs rounded',
md: 'px-2 py-1 text-xs rounded-md'
};
return (
<span
className={cn(
baseStyles,
variants[variant],
sizes[size],
className
)}
ref={ref}
{...props}
/>
);
}
);
Badge.displayName = 'Badge';
export { Badge };

43
components/ui/Button.tsx Normal file
View File

@@ -0,0 +1,43 @@
import { ButtonHTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'primary', size = 'md', ...props }, ref) => {
const baseStyles = 'inline-flex items-center justify-center font-mono font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-950 disabled:opacity-50 disabled:cursor-not-allowed';
const variants = {
primary: 'bg-cyan-600 hover:bg-cyan-500 text-white border border-cyan-500/30 shadow-cyan-500/20 shadow-lg hover:shadow-cyan-500/30 focus:ring-cyan-500',
secondary: 'bg-slate-800 hover:bg-slate-700 text-slate-100 border border-slate-600 shadow-slate-500/20 shadow-lg hover:shadow-slate-500/30 focus:ring-slate-500',
danger: 'bg-red-600 hover:bg-red-500 text-white border border-red-500/30 shadow-red-500/20 shadow-lg hover:shadow-red-500/30 focus:ring-red-500',
ghost: 'bg-transparent hover:bg-slate-800/50 text-slate-300 hover:text-slate-100 border border-slate-700/50 hover:border-slate-600 focus:ring-slate-500'
};
const sizes = {
sm: 'px-3 py-1.5 text-xs rounded-md',
md: 'px-4 py-2 text-sm rounded-lg',
lg: 'px-6 py-3 text-base rounded-lg'
};
return (
<button
className={cn(
baseStyles,
variants[variant],
sizes[size],
className
)}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button };

80
components/ui/Card.tsx Normal file
View File

@@ -0,0 +1,80 @@
import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface CardProps extends HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'elevated' | 'bordered';
}
const Card = forwardRef<HTMLDivElement, CardProps>(
({ className, variant = 'default', ...props }, ref) => {
const variants = {
default: 'bg-slate-800/50 border border-slate-700/50',
elevated: 'bg-slate-800/80 border border-slate-700/50 shadow-lg shadow-slate-900/20',
bordered: 'bg-slate-900/50 border border-cyan-500/30 shadow-cyan-500/10 shadow-lg'
};
return (
<div
ref={ref}
className={cn(
'rounded-lg backdrop-blur-sm transition-all duration-200',
variants[variant],
className
)}
{...props}
/>
);
}
);
Card.displayName = 'Card';
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('p-4 border-b border-slate-700/50', className)}
{...props}
/>
)
);
CardHeader.displayName = 'CardHeader';
const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn('font-mono font-semibold text-slate-100 tracking-wide', className)}
{...props}
/>
)
);
CardTitle.displayName = 'CardTitle';
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('p-4', className)}
{...props}
/>
)
);
CardContent.displayName = 'CardContent';
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('p-4 border-t border-slate-700/50', className)}
{...props}
/>
)
);
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardTitle, CardContent, CardFooter };

View File

@@ -1,3 +1,5 @@
import { Card, CardContent } from '@/components/ui/Card';
interface HeaderProps {
title: string;
subtitle: string;
@@ -69,49 +71,25 @@ interface StatCardProps {
}
function StatCard({ label, value, color }: StatCardProps) {
const techStyles = {
blue: {
bg: 'bg-slate-800/50',
border: 'border-cyan-500/30',
text: 'text-cyan-300',
glow: 'shadow-cyan-500/20'
},
green: {
bg: 'bg-slate-800/50',
border: 'border-emerald-500/30',
text: 'text-emerald-300',
glow: 'shadow-emerald-500/20'
},
yellow: {
bg: 'bg-slate-800/50',
border: 'border-yellow-500/30',
text: 'text-yellow-300',
glow: 'shadow-yellow-500/20'
},
gray: {
bg: 'bg-slate-800/50',
border: 'border-slate-500/30',
text: 'text-slate-300',
glow: 'shadow-slate-500/20'
},
purple: {
bg: 'bg-slate-800/50',
border: 'border-purple-500/30',
text: 'text-purple-300',
glow: 'shadow-purple-500/20'
}
const textColors = {
blue: 'text-cyan-300',
green: 'text-emerald-300',
yellow: 'text-yellow-300',
gray: 'text-slate-300',
purple: 'text-purple-300'
};
const style = techStyles[color];
return (
<div className={`${style.bg} ${style.border} border rounded-lg px-3 py-2 ${style.glow} shadow-lg backdrop-blur-sm hover:${style.border.replace('/30', '/50')} transition-all duration-300`}>
<div className={`text-xs font-mono font-bold ${style.text} opacity-75 uppercase tracking-wider`}>
{label}
</div>
<div className={`text-lg font-mono font-bold ${style.text}`}>
{value}
</div>
</div>
<Card variant="elevated" className="px-3 py-2 hover:border-opacity-75 transition-all duration-300">
<CardContent className="p-0">
<div className={`text-xs font-mono font-bold ${textColors[color]} opacity-75 uppercase tracking-wider`}>
{label}
</div>
<div className={`text-lg font-mono font-bold ${textColors[color]}`}>
{value}
</div>
</CardContent>
</Card>
);
}

44
components/ui/Input.tsx Normal file
View File

@@ -0,0 +1,44 @@
import { InputHTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, label, error, ...props }, ref) => {
return (
<div className="space-y-2">
{label && (
<label className="block text-sm font-mono font-medium text-slate-300 uppercase tracking-wider">
{label}
</label>
)}
<input
className={cn(
'w-full px-3 py-2 bg-slate-800/50 border border-slate-700/50 rounded-lg',
'text-slate-100 font-mono text-sm placeholder-slate-500',
'focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50',
'hover:border-slate-600/50 transition-all duration-200',
'backdrop-blur-sm',
error && 'border-red-500/50 focus:ring-red-500/50 focus:border-red-500/50',
className
)}
ref={ref}
{...props}
/>
{error && (
<p className="text-xs font-mono text-red-400 flex items-center gap-1">
<span className="text-red-500"></span>
{error}
</p>
)}
</div>
);
}
);
Input.displayName = 'Input';
export { Input };

78
components/ui/Modal.tsx Normal file
View File

@@ -0,0 +1,78 @@
'use client';
import { ReactNode, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { cn } from '@/lib/utils';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: ReactNode;
title?: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
}
export function Modal({ isOpen, onClose, children, title, size = 'md' }: ModalProps) {
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [isOpen, onClose]);
if (!isOpen) return null;
const sizes = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl'
};
return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-slate-950/80 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className={cn(
'relative w-full mx-4 bg-slate-900/95 border border-slate-700/50 rounded-lg shadow-2xl shadow-slate-900/50 backdrop-blur-sm',
sizes[size]
)}>
{/* Header */}
{title && (
<div className="flex items-center justify-between p-4 border-b border-slate-700/50">
<h2 className="text-lg font-mono font-semibold text-slate-100 tracking-wide">
{title}
</h2>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-100 transition-colors p-1"
>
<span className="sr-only">Fermer</span>
</button>
</div>
)}
{/* Content */}
<div className="p-4">
{children}
</div>
</div>
</div>,
document.body
);
}