feat: unify CardHeader padding across components
- Updated `CardHeader` padding from `pb-3` to `pb-4` in `JiraLogs`, `JiraSync`, `KanbanColumn`, `ObjectivesBoard`, and `DesktopControls` for consistent spacing. - Refactored `DesktopControls` and `KanbanFilters` to utilize new `ControlPanel`, `ControlSection`, and `ControlGroup` components, enhancing layout structure and maintainability. - Replaced button elements with `ToggleButton` and `FilterChip` components in various filter sections for improved UI consistency and usability.
This commit is contained in:
46
src/components/ui/ControlPanel.tsx
Normal file
46
src/components/ui/ControlPanel.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ControlPanelProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ControlPanel({ children, className }: ControlPanelProps) {
|
||||
return (
|
||||
<div className={cn(
|
||||
'bg-[var(--card)]/30 border-b border-[var(--border)]/30 w-full',
|
||||
className
|
||||
)}>
|
||||
<div className="w-full px-6 py-2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ControlSectionProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ControlSection({ children, className }: ControlSectionProps) {
|
||||
return (
|
||||
<div className={cn('flex items-center gap-4', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ControlGroupProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ControlGroup({ children, className }: ControlGroupProps) {
|
||||
return (
|
||||
<div className={cn('flex items-center gap-2', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
75
src/components/ui/FilterChip.tsx
Normal file
75
src/components/ui/FilterChip.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface FilterChipProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'default' | 'selected' | 'hidden' | 'priority' | 'tag';
|
||||
color?: string;
|
||||
count?: number;
|
||||
icon?: React.ReactNode;
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
|
||||
const FilterChip = forwardRef<HTMLButtonElement, FilterChipProps>(
|
||||
({
|
||||
className,
|
||||
variant = 'default',
|
||||
color,
|
||||
count,
|
||||
icon,
|
||||
size = 'sm',
|
||||
children,
|
||||
...props
|
||||
}, ref) => {
|
||||
const variants = {
|
||||
default: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)] hover:bg-[var(--card)]/80',
|
||||
selected: 'border-cyan-400 bg-cyan-400/10 text-cyan-400',
|
||||
hidden: 'bg-[var(--muted)]/20 text-[var(--muted)] border-[var(--muted)]/30 hover:bg-[var(--muted)]/30',
|
||||
priority: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]',
|
||||
tag: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-2 py-1 text-xs',
|
||||
md: 'px-3 py-1.5 text-sm'
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded border transition-all font-medium cursor-pointer',
|
||||
variants[variant],
|
||||
sizes[size],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{icon && (
|
||||
<div className="flex-shrink-0">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{color && (
|
||||
<div
|
||||
className="w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span className="flex-1 text-left">
|
||||
{children}
|
||||
{count !== undefined && count > 0 && (
|
||||
<span className="ml-1 opacity-75">
|
||||
({count})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FilterChip.displayName = 'FilterChip';
|
||||
|
||||
export { FilterChip };
|
||||
168
src/components/ui/FilterSummary.tsx
Normal file
168
src/components/ui/FilterSummary.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './Card';
|
||||
import { Badge } from './Badge';
|
||||
import { Button } from './Button';
|
||||
|
||||
interface FilterSummaryProps {
|
||||
filters: {
|
||||
search?: string;
|
||||
priorities?: string[];
|
||||
tags?: string[];
|
||||
showWithDueDate?: boolean;
|
||||
showJiraOnly?: boolean;
|
||||
hideJiraTasks?: boolean;
|
||||
jiraProjects?: string[];
|
||||
jiraTypes?: string[];
|
||||
showTfsOnly?: boolean;
|
||||
hideTfsTasks?: boolean;
|
||||
tfsProjects?: string[];
|
||||
};
|
||||
activeFiltersCount: number;
|
||||
onClearFilters?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FilterSummary({
|
||||
filters,
|
||||
activeFiltersCount,
|
||||
onClearFilters,
|
||||
className
|
||||
}: FilterSummaryProps) {
|
||||
if (activeFiltersCount === 0) return null;
|
||||
|
||||
const filterItems: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
variant: 'default' | 'primary' | 'success' | 'destructive' | 'accent' | 'purple' | 'yellow' | 'green' | 'blue' | 'gray' | 'outline' | 'danger' | 'warning';
|
||||
}> = [];
|
||||
|
||||
// Recherche
|
||||
if (filters.search) {
|
||||
filterItems.push({
|
||||
label: 'Recherche',
|
||||
value: `"${filters.search}"`,
|
||||
variant: 'primary'
|
||||
});
|
||||
}
|
||||
|
||||
// Priorités
|
||||
if (filters.priorities?.filter(Boolean).length) {
|
||||
filterItems.push({
|
||||
label: 'Priorités',
|
||||
value: filters.priorities.filter(Boolean).join(', '),
|
||||
variant: 'accent'
|
||||
});
|
||||
}
|
||||
|
||||
// Tags
|
||||
if (filters.tags?.filter(Boolean).length) {
|
||||
filterItems.push({
|
||||
label: 'Tags',
|
||||
value: filters.tags.filter(Boolean).join(', '),
|
||||
variant: 'purple'
|
||||
});
|
||||
}
|
||||
|
||||
// Affichage avec date de fin
|
||||
if (filters.showWithDueDate) {
|
||||
filterItems.push({
|
||||
label: 'Affichage',
|
||||
value: 'Avec date de fin',
|
||||
variant: 'success'
|
||||
});
|
||||
}
|
||||
|
||||
// Jira
|
||||
if (filters.showJiraOnly) {
|
||||
filterItems.push({
|
||||
label: 'Affichage',
|
||||
value: 'Jira seulement',
|
||||
variant: 'blue'
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.hideJiraTasks) {
|
||||
filterItems.push({
|
||||
label: 'Affichage',
|
||||
value: 'Masquer Jira',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.jiraProjects?.filter(Boolean).length) {
|
||||
filterItems.push({
|
||||
label: 'Projets Jira',
|
||||
value: filters.jiraProjects.filter(Boolean).join(', '),
|
||||
variant: 'blue'
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.jiraTypes?.filter(Boolean).length) {
|
||||
filterItems.push({
|
||||
label: 'Types Jira',
|
||||
value: filters.jiraTypes.filter(Boolean).join(', '),
|
||||
variant: 'purple'
|
||||
});
|
||||
}
|
||||
|
||||
// TFS
|
||||
if (filters.showTfsOnly) {
|
||||
filterItems.push({
|
||||
label: 'Affichage',
|
||||
value: 'TFS seulement',
|
||||
variant: 'yellow'
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.hideTfsTasks) {
|
||||
filterItems.push({
|
||||
label: 'Affichage',
|
||||
value: 'Masquer TFS',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.tfsProjects?.filter(Boolean).length) {
|
||||
filterItems.push({
|
||||
label: 'Projets TFS',
|
||||
value: filters.tfsProjects.filter(Boolean).join(', '),
|
||||
variant: 'yellow'
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader className="py-2 pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xs font-mono uppercase tracking-wider text-[var(--muted-foreground)]">
|
||||
Filtres actifs ({activeFiltersCount})
|
||||
</CardTitle>
|
||||
{onClearFilters && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClearFilters}
|
||||
className="text-[var(--muted-foreground)] hover:text-[var(--destructive)] text-xs"
|
||||
>
|
||||
Effacer
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-3">
|
||||
<div className="space-y-2">
|
||||
{filterItems.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-2 text-xs">
|
||||
<span className="text-[var(--muted-foreground)] font-medium">
|
||||
{item.label}:
|
||||
</span>
|
||||
<Badge variant={item.variant} size="sm">
|
||||
{item.value}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
78
src/components/ui/SearchInput.tsx
Normal file
78
src/components/ui/SearchInput.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { InputHTMLAttributes, forwardRef, useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Input } from './Input';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface SearchInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onDebouncedChange?: (value: string) => void;
|
||||
debounceMs?: number;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
||||
({
|
||||
value = '',
|
||||
onChange,
|
||||
onDebouncedChange,
|
||||
debounceMs = 300,
|
||||
placeholder = "Rechercher...",
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
const timeoutRef = useRef<number | undefined>(undefined);
|
||||
|
||||
// Fonction debouncée pour les changements
|
||||
const debouncedChange = useCallback((searchValue: string) => {
|
||||
if (timeoutRef.current) {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
onDebouncedChange?.(searchValue);
|
||||
}, debounceMs);
|
||||
}, [onDebouncedChange, debounceMs]);
|
||||
|
||||
// Gérer les changements locaux
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setLocalValue(newValue);
|
||||
onChange?.(newValue);
|
||||
debouncedChange(newValue);
|
||||
};
|
||||
|
||||
// Synchroniser l'état local quand la valeur externe change
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
// Nettoyer le timeout au démontage
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cn('flex-1 min-w-0', className)}>
|
||||
<Input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
className="bg-[var(--card)] border-[var(--border)] w-full"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SearchInput.displayName = 'SearchInput';
|
||||
|
||||
export { SearchInput };
|
||||
78
src/components/ui/ToggleButton.tsx
Normal file
78
src/components/ui/ToggleButton.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ToggleButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'accent' | 'secondary' | 'warning' | 'cyan';
|
||||
size?: 'sm' | 'md';
|
||||
isActive?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(
|
||||
({ className, variant = 'primary', size = 'md', isActive = false, icon, count, children, ...props }, ref) => {
|
||||
const variants = {
|
||||
primary: isActive
|
||||
? 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30'
|
||||
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--primary)]/50',
|
||||
accent: isActive
|
||||
? 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30'
|
||||
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--accent)]/50',
|
||||
secondary: isActive
|
||||
? 'bg-[var(--secondary)]/20 text-[var(--secondary)] border border-[var(--secondary)]/30'
|
||||
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--secondary)]/50',
|
||||
warning: isActive
|
||||
? 'bg-[var(--warning)]/20 text-[var(--warning)] border border-[var(--warning)]/30'
|
||||
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--warning)]/50',
|
||||
cyan: isActive
|
||||
? 'bg-[var(--cyan)]/20 text-[var(--cyan)] border border-[var(--cyan)]/30'
|
||||
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--cyan)]/50'
|
||||
};
|
||||
|
||||
// Déterminer si c'est un bouton avec seulement des icônes
|
||||
const isIconOnly = icon && !children && count === undefined;
|
||||
|
||||
const sizes = {
|
||||
sm: isIconOnly ? 'px-2 py-1.5 text-sm' : 'px-3 py-1.5 text-sm',
|
||||
md: isIconOnly ? 'px-2 py-1.5 text-sm' : 'px-3 py-1.5 text-sm'
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md font-mono transition-all',
|
||||
variants[variant],
|
||||
sizes[size],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{icon && (
|
||||
<div className="flex-shrink-0">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
{children && (
|
||||
<span className="flex-1 text-left">
|
||||
{children}
|
||||
{count !== undefined && count > 0 && (
|
||||
<span className="ml-1 text-xs opacity-75">
|
||||
({count})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{count !== undefined && count > 0 && !children && (
|
||||
<span className="text-xs opacity-75">
|
||||
({count})
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ToggleButton.displayName = 'ToggleButton';
|
||||
|
||||
export { ToggleButton };
|
||||
@@ -12,6 +12,13 @@ export { ActionCard } from './ActionCard';
|
||||
export { TaskCard } from './TaskCard';
|
||||
export { MetricCard } from './MetricCard';
|
||||
|
||||
// Composants Kanban
|
||||
export { ToggleButton } from './ToggleButton';
|
||||
export { SearchInput } from './SearchInput';
|
||||
export { ControlPanel, ControlSection, ControlGroup } from './ControlPanel';
|
||||
export { FilterSummary } from './FilterSummary';
|
||||
export { FilterChip } from './FilterChip';
|
||||
|
||||
// Composants existants
|
||||
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
|
||||
export { Header } from './Header';
|
||||
|
||||
Reference in New Issue
Block a user