feat: implement theme system and UI updates
- Added theme context and provider for light/dark mode support. - Integrated theme toggle button in the Header component. - Updated UI components to utilize CSS variables for consistent theming. - Enhanced Kanban components and forms with new theme styles for better visual coherence. - Adjusted global styles to define color variables for both themes, improving maintainability.
This commit is contained in:
@@ -11,12 +11,12 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
||||
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'
|
||||
default: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)]',
|
||||
primary: 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30',
|
||||
success: 'bg-[var(--success)]/20 text-[var(--success)] border border-[var(--success)]/30',
|
||||
warning: 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30',
|
||||
danger: 'bg-[var(--destructive)]/20 text-[var(--destructive)] border border-[var(--destructive)]/30',
|
||||
outline: 'bg-transparent text-[var(--muted-foreground)] border border-[var(--border)] hover:bg-[var(--card-hover)] hover:text-[var(--foreground)]'
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
|
||||
@@ -8,13 +8,13 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
|
||||
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 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-[var(--background)] 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'
|
||||
primary: 'bg-[var(--primary)] hover:bg-[var(--primary)]/80 text-[var(--primary-foreground)] border border-[var(--primary)]/30 shadow-[var(--primary)]/20 shadow-lg hover:shadow-[var(--primary)]/30 focus:ring-[var(--primary)]',
|
||||
secondary: 'bg-[var(--card)] hover:bg-[var(--card-hover)] text-[var(--foreground)] border border-[var(--border)] shadow-[var(--muted)]/20 shadow-lg hover:shadow-[var(--muted)]/30 focus:ring-[var(--muted)]',
|
||||
danger: 'bg-[var(--destructive)] hover:bg-[var(--destructive)]/80 text-white border border-[var(--destructive)]/30 shadow-[var(--destructive)]/20 shadow-lg hover:shadow-[var(--destructive)]/30 focus:ring-[var(--destructive)]',
|
||||
ghost: 'bg-transparent hover:bg-[var(--card)]/50 text-[var(--muted-foreground)] hover:text-[var(--foreground)] border border-[var(--border)]/50 hover:border-[var(--border)] focus:ring-[var(--muted)]'
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
|
||||
@@ -2,15 +2,16 @@ import { HTMLAttributes, forwardRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
variant?: 'default' | 'elevated' | 'bordered';
|
||||
variant?: 'default' | 'elevated' | 'bordered' | 'column';
|
||||
}
|
||||
|
||||
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'
|
||||
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'
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -33,7 +34,7 @@ const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('p-4 border-b border-slate-700/50', className)}
|
||||
className={cn('p-4 border-b border-[var(--border)]/50', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -45,7 +46,7 @@ const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingEleme
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn('font-mono font-semibold text-slate-100 tracking-wide', className)}
|
||||
className={cn('font-mono font-semibold text-[var(--foreground)] tracking-wide', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -69,7 +70,7 @@ const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('p-4 border-t border-slate-700/50', className)}
|
||||
className={cn('p-4 border-t border-[var(--border)]/50', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
import { TaskStats } from '@/lib/types';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface HeaderProps {
|
||||
@@ -10,8 +11,10 @@ interface HeaderProps {
|
||||
}
|
||||
|
||||
export function Header({ title, subtitle, stats, syncing = false }: HeaderProps) {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<header className="bg-slate-900/80 backdrop-blur-sm border-b border-slate-700/50 shadow-lg shadow-slate-900/20">
|
||||
<header className="bg-[var(--card)]/80 backdrop-blur-sm border-b border-[var(--border)]/50 shadow-lg shadow-[var(--card)]/20">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-6">
|
||||
{/* Titre tech avec glow */}
|
||||
@@ -23,10 +26,10 @@ export function Header({ title, subtitle, stats, syncing = false }: HeaderProps)
|
||||
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
|
||||
}`}></div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-mono font-bold text-slate-100 tracking-wider">
|
||||
<h1 className="text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-slate-400 mt-1 font-mono text-sm">
|
||||
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-sm">
|
||||
{subtitle} {syncing && '• Synchronisation...'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -36,16 +39,33 @@ export function Header({ title, subtitle, stats, syncing = false }: HeaderProps)
|
||||
<nav className="hidden sm:flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-slate-400 hover:text-cyan-400 transition-colors font-mono text-sm uppercase tracking-wider"
|
||||
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors font-mono text-sm uppercase tracking-wider"
|
||||
>
|
||||
Kanban
|
||||
</Link>
|
||||
<Link
|
||||
href="/tags"
|
||||
className="text-slate-400 hover:text-purple-400 transition-colors font-mono text-sm uppercase tracking-wider"
|
||||
className="text-[var(--muted-foreground)] hover:text-[var(--accent)] transition-colors font-mono text-sm uppercase tracking-wider"
|
||||
>
|
||||
Tags
|
||||
</Link>
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-1 rounded-md hover:bg-[var(--card-hover)]"
|
||||
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -119,11 +139,11 @@ interface StatCardProps {
|
||||
function StatCard({ label, value, color }: StatCardProps) {
|
||||
|
||||
const textColors = {
|
||||
blue: 'text-cyan-300',
|
||||
green: 'text-emerald-300',
|
||||
yellow: 'text-yellow-300',
|
||||
gray: 'text-slate-300',
|
||||
purple: 'text-purple-300'
|
||||
blue: 'text-[var(--primary)]',
|
||||
green: 'text-[var(--success)]',
|
||||
yellow: 'text-[var(--accent)]',
|
||||
gray: 'text-[var(--muted-foreground)]',
|
||||
purple: 'text-[var(--accent)]'
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -11,26 +11,26 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{label && (
|
||||
<label className="block text-sm font-mono font-medium text-slate-300 uppercase tracking-wider">
|
||||
<label className="block text-sm font-mono font-medium text-[var(--muted-foreground)] 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',
|
||||
'w-full px-3 py-2 bg-[var(--input)] border border-[var(--border)]/50 rounded-lg',
|
||||
'text-[var(--foreground)] font-mono text-sm placeholder-[var(--muted-foreground)]',
|
||||
'focus:outline-none focus:ring-2 focus:ring-[var(--primary)]/50 focus:border-[var(--primary)]/50',
|
||||
'hover:border-[var(--border)] transition-all duration-200',
|
||||
'backdrop-blur-sm',
|
||||
error && 'border-red-500/50 focus:ring-red-500/50 focus:border-red-500/50',
|
||||
error && 'border-[var(--destructive)]/50 focus:ring-[var(--destructive)]/50 focus:border-[var(--destructive)]/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>
|
||||
<p className="text-xs font-mono text-[var(--destructive)] flex items-center gap-1">
|
||||
<span className="text-[var(--destructive)]">⚠</span>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -42,24 +42,24 @@ export function Modal({ isOpen, onClose, children, title, size = 'md' }: ModalPr
|
||||
<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"
|
||||
className="absolute inset-0 bg-[var(--background)]/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',
|
||||
'relative w-full mx-4 bg-[var(--card)]/95 border border-[var(--border)]/50 rounded-lg shadow-2xl shadow-[var(--card)]/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">
|
||||
<div className="flex items-center justify-between p-4 border-b border-[var(--border)]/50">
|
||||
<h2 className="text-lg font-mono font-semibold text-[var(--foreground)] tracking-wide">
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-slate-400 hover:text-slate-100 transition-colors p-1"
|
||||
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors p-1"
|
||||
>
|
||||
<span className="sr-only">Fermer</span>
|
||||
✕
|
||||
|
||||
@@ -108,7 +108,7 @@ export function TagInput({
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
{/* Container des tags et input */}
|
||||
<div className="min-h-[42px] p-2 border border-slate-600 rounded-lg bg-slate-800 focus-within:border-cyan-400 focus-within:ring-1 focus-within:ring-cyan-400/20 transition-colors">
|
||||
<div className="min-h-[42px] p-2 border border-[var(--border)] rounded-lg bg-[var(--input)] focus-within:border-[var(--primary)] focus-within:ring-1 focus-within:ring-[var(--primary)]/20 transition-colors">
|
||||
<div className="flex flex-wrap gap-1 items-center">
|
||||
{/* Tags existants */}
|
||||
{tags.map((tag, index) => (
|
||||
@@ -121,7 +121,7 @@ export function TagInput({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeTag(tag)}
|
||||
className="text-slate-400 hover:text-slate-200 ml-1"
|
||||
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] ml-1"
|
||||
aria-label={`Supprimer le tag ${tag}`}
|
||||
>
|
||||
×
|
||||
@@ -140,7 +140,7 @@ export function TagInput({
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
placeholder={tags.length === 0 ? placeholder : ""}
|
||||
className="flex-1 min-w-[120px] bg-transparent border-none outline-none text-slate-100 placeholder-slate-400 text-sm"
|
||||
className="flex-1 min-w-[120px] bg-transparent border-none outline-none text-[var(--foreground)] placeholder-[var(--muted-foreground)] text-sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -150,10 +150,10 @@ export function TagInput({
|
||||
{showSuggestions && (suggestions.length > 0 || loading) && (
|
||||
<div
|
||||
ref={suggestionsRef}
|
||||
className="absolute top-full left-0 right-0 mt-1 bg-slate-800 border border-slate-600 rounded-lg shadow-lg z-50 max-h-64 overflow-y-auto"
|
||||
className="absolute top-full left-0 right-0 mt-1 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-50 max-h-64 overflow-y-auto"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="p-3 text-center text-slate-400 text-sm">
|
||||
<div className="p-3 text-center text-[var(--muted-foreground)] text-sm">
|
||||
Recherche...
|
||||
</div>
|
||||
) : (
|
||||
@@ -165,8 +165,8 @@ export function TagInput({
|
||||
onClick={() => handleSuggestionClick(tag)}
|
||||
className={`flex items-center gap-2 px-2 py-1.5 text-xs rounded-md transition-colors ${
|
||||
index === selectedIndex
|
||||
? 'bg-slate-700 text-cyan-300 ring-1 ring-cyan-400'
|
||||
: 'text-slate-200 hover:bg-slate-700'
|
||||
? 'bg-[var(--card-hover)] text-[var(--primary)] ring-1 ring-[var(--primary)]'
|
||||
: 'text-[var(--foreground)] hover:bg-[var(--card-hover)]'
|
||||
} ${tags.includes(tag.name) ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
disabled={tags.includes(tag.name)}
|
||||
title={tags.includes(tag.name) ? 'Déjà ajouté' : `Ajouter ${tag.name}`}
|
||||
@@ -177,7 +177,7 @@ export function TagInput({
|
||||
/>
|
||||
<span className="truncate">{tag.name}</span>
|
||||
{tags.includes(tag.name) && (
|
||||
<span className="text-slate-400 ml-auto">✓</span>
|
||||
<span className="text-[var(--muted-foreground)] ml-auto">✓</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
@@ -188,7 +188,7 @@ export function TagInput({
|
||||
|
||||
{/* Indicateur de limite */}
|
||||
{tags.length >= maxTags && (
|
||||
<div className="text-xs text-slate-400 mt-1">
|
||||
<div className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
Limite de {maxTags} tags atteinte
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user