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:
Julien Froidefond
2025-09-28 21:53:22 +02:00
parent bdf8ab9fb4
commit 0fcd4d68c1
20 changed files with 1011 additions and 451 deletions

View File

@@ -62,7 +62,7 @@ export function JiraLogs({ className = "" }: JiraLogsProps) {
return (
<Card className={className}>
<CardHeader className="pb-3">
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-gray-400 animate-pulse"></div>

View File

@@ -147,7 +147,7 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
return (
<Card className={`${className}`}>
<CardHeader className="pb-3">
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full animate-pulse" style={{ backgroundColor: 'var(--blue)' }}></div>

View File

@@ -38,7 +38,7 @@ export function KanbanColumn({ id, tasks, onCreateTask, onEditTask, compactView
isOver ? 'ring-2 ring-[var(--primary)]/50 bg-[var(--card-hover)]' : ''
}`}
>
<CardHeader className="pb-3">
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${style.accent.replace('text-', 'bg-')} animate-pulse`}></div>

View File

@@ -1,8 +1,7 @@
'use client';
import { useState, useEffect, useRef, useCallback } from 'react';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Button, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup } from '@/components/ui';
import { SourceQuickFilter } from '@/components/kanban/SourceQuickFilter';
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
import type { KanbanFilters } from '@/lib/types';
@@ -77,86 +76,75 @@ export function DesktopControls({
});
};
return (
<div className="bg-[var(--card)]/30 border-b border-[var(--border)]/30 w-full">
<div className="w-full px-6 py-2">
{/* Layout responsive : deux lignes sur tablette, une ligne sur desktop */}
<div className="flex flex-col lg:flex-row lg:items-center gap-4 lg:gap-0 w-full">
{/* Section gauche : Recherche + Boutons principaux */}
<div className="flex items-center gap-4">
{/* Champ de recherche */}
<div className="flex-1 min-w-0">
<Input
type="text"
value={localSearch}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder="Rechercher des tâches..."
className="bg-[var(--card)] border-[var(--border)] w-full"
/>
</div>
<div className="flex items-center gap-2">
<button
onClick={onToggleFilters}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
showFilters
? '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'
}`}
>
<ControlPanel>
{/* Layout responsive : deux lignes sur tablette, une ligne sur desktop */}
<div className="flex flex-col lg:flex-row lg:items-center gap-4 lg:gap-0 w-full">
{/* Section gauche : Recherche + Boutons principaux */}
<ControlSection>
{/* Champ de recherche */}
<SearchInput
value={localSearch}
onChange={handleSearchChange}
placeholder="Rechercher des tâches..."
/>
<ControlGroup>
<ToggleButton
variant="primary"
isActive={showFilters}
count={activeFiltersCount}
onClick={onToggleFilters}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
</svg>
Filtres{activeFiltersCount > 0 && ` (${activeFiltersCount})`}
</button>
<button
onClick={onToggleObjectives}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
showObjectives
? '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'
}`}
>
}
>
Filtres
</ToggleButton>
<ToggleButton
variant="accent"
isActive={showObjectives}
onClick={onToggleObjectives}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
Objectifs
</button>
<button
onClick={handleDueDateFilterToggle}
className={`flex items-center justify-center px-3 py-1.5 rounded-md transition-all mr-4 ${
kanbanFilters.showWithDueDate
? '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'
}`}
title={kanbanFilters.showWithDueDate ? "Afficher toutes les tâches" : "Afficher seulement les tâches avec date de fin"}
>
}
>
Objectifs
</ToggleButton>
<ToggleButton
variant="cyan"
isActive={kanbanFilters.showWithDueDate}
onClick={handleDueDateFilterToggle}
title={kanbanFilters.showWithDueDate ? "Afficher toutes les tâches" : "Afficher seulement les tâches avec date de fin"}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 002 2v12a2 2 0 002 2z" />
</svg>
</button>
</div>
</div>
}
/>
</ControlGroup>
</ControlSection>
{/* Section droite : Raccourcis + Bouton Nouvelle tâche */}
<div className="flex items-center justify-between lg:justify-start gap-4">
<div className="flex items-center gap-2 border-l border-[var(--border)] pl-4">
{/* Raccourcis Sources (Jira & TFS) */}
<SourceQuickFilter
filters={kanbanFilters}
onFiltersChange={onFiltersChange}
/>
<button
onClick={onToggleCompactView}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
compactView
? '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'
}`}
title={compactView ? "Vue détaillée" : "Vue compacte"}
>
{/* Section droite : Raccourcis + Bouton Nouvelle tâche */}
<ControlSection className="justify-between lg:justify-start">
<ControlGroup className="border-l border-[var(--border)] ml-2 pl-2 pr-4">
{/* Raccourcis Sources (Jira & TFS) */}
<SourceQuickFilter
filters={kanbanFilters}
onFiltersChange={onFiltersChange}
/>
<ToggleButton
variant="secondary"
isActive={compactView}
onClick={onToggleCompactView}
title={compactView ? "Vue détaillée" : "Vue compacte"}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{compactView ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
@@ -164,18 +152,17 @@ export function DesktopControls({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
{compactView ? 'Détaillée' : 'Compacte'}
</button>
<button
onClick={onToggleSwimlanes}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
swimlanesByTags
? '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'
}`}
title={swimlanesByTags ? "Vue standard" : "Vue swimlanes"}
>
}
>
{compactView ? 'Détaillée' : 'Compacte'}
</ToggleButton>
<ToggleButton
variant="warning"
isActive={swimlanesByTags}
onClick={onToggleSwimlanes}
title={swimlanesByTags ? "Vue standard" : "Vue swimlanes"}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{swimlanesByTags ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
@@ -183,28 +170,29 @@ export function DesktopControls({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14-7H5m14 14H5" />
)}
</svg>
{swimlanesByTags ? 'Standard' : 'Swimlanes'}
</button>
{/* Font Size Toggle */}
<FontSizeToggle />
</div>
{/* Bouton d'ajout de tâche */}
<Button
variant="primary"
size="sm"
onClick={onCreateTask}
className="flex items-center gap-2"
}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Nouvelle tâche
</Button>
</div>
</div>
{swimlanesByTags ? 'Standard' : 'Swimlanes'}
</ToggleButton>
{/* Font Size Toggle */}
<FontSizeToggle />
</ControlGroup>
{/* Bouton d'ajout de tâche */}
<Button
variant="primary"
size="sm"
onClick={onCreateTask}
className="flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Nouvelle tâche
</Button>
</ControlSection>
</div>
</div>
</ControlPanel>
);
}

View File

@@ -1,10 +1,9 @@
'use client';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useState, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { TaskPriority, TaskStatus } from '@/lib/types';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Button, SearchInput, ToggleButton, ControlPanel, ControlSection, ControlGroup, FilterSummary } from '@/components/ui';
import { useTasksContext } from '@/contexts/TasksContext';
import { SORT_OPTIONS } from '@/lib/sort-config';
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
@@ -57,47 +56,11 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
}
}, [isSortExpanded, isSwimlaneModeExpanded]);
// État local pour la recherche pour une saisie fluide
const [localSearch, setLocalSearch] = useState(filters.search || '');
const searchTimeoutRef = useRef<number | undefined>(undefined);
// Fonction debouncée pour mettre à jour les filtres
const debouncedSearchChange = useCallback((search: string) => {
if (searchTimeoutRef.current) {
window.clearTimeout(searchTimeoutRef.current);
}
searchTimeoutRef.current = window.setTimeout(() => {
onFiltersChange({ ...filters, search: search || undefined });
}, 300);
}, [filters, onFiltersChange]);
// Handler pour la recherche avec debounce intégré
const handleSearchChange = (search: string) => {
isUserTypingRef.current = true;
setLocalSearch(search);
debouncedSearchChange(search);
onFiltersChange({ ...filters, search: search || undefined });
};
// Synchroniser l'état local quand les filtres changent de l'extérieur
// mais seulement si ce n'est pas dû à notre propre saisie
const isUserTypingRef = useRef(false);
useEffect(() => {
if (!isUserTypingRef.current) {
setLocalSearch(filters.search || '');
}
isUserTypingRef.current = false;
}, [filters.search]);
// Nettoyer le timeout au démontage
useEffect(() => {
return () => {
if (searchTimeoutRef.current) {
window.clearTimeout(searchTimeoutRef.current);
}
};
}, []);
const handleTagToggle = (tagName: string) => {
const currentTags = filters.tags || [];
const newTags = currentTags.includes(tagName)
@@ -180,48 +143,48 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
};
return (
<div className="bg-[var(--card)]/50 border-b border-[var(--border)]/50 backdrop-blur-sm">
<ControlPanel className="bg-[var(--card)]/50 border-b border-[var(--border)]/50 backdrop-blur-sm">
<div className="container mx-auto px-6 py-4">
{/* Header avec recherche et bouton expand */}
<div className="flex items-center gap-4">
<ControlSection>
<div className="flex-1 max-w-md">
<Input
type="text"
value={localSearch}
onChange={(e) => handleSearchChange(e.target.value)}
<SearchInput
value={filters.search || ''}
onChange={handleSearchChange}
placeholder="Rechercher des tâches..."
className="bg-[var(--card)] border-[var(--border)]"
/>
</div>
{/* Menu swimlanes - masqué sur mobile */}
{!isMobile && (
<div className="flex gap-1">
<Button
variant={filters.swimlanesByTags ? "primary" : "ghost"}
<ControlGroup>
<ToggleButton
variant="warning"
isActive={!!filters.swimlanesByTags}
onClick={handleSwimlanesToggle}
className="flex items-center gap-2"
title="Mode d'affichage"
icon={
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{filters.swimlanesByTags ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
)}
</svg>
}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{filters.swimlanesByTags ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
)}
</svg>
{!filters.swimlanesByTags
? 'Normal'
: filters.swimlanesMode === 'priority'
? 'Par priorité'
: 'Par tags'
}
</Button>
</ToggleButton>
{/* Bouton pour changer le mode des swimlanes */}
{filters.swimlanesByTags && (
@@ -240,7 +203,7 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
</svg>
</Button>
)}
</div>
</ControlGroup>
)}
@@ -273,16 +236,7 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
</div>
{activeFiltersCount > 0 && (
<Button
variant="ghost"
onClick={handleClearFilters}
className="text-[var(--muted-foreground)] hover:text-[var(--destructive)]"
>
Effacer
</Button>
)}
</div>
</ControlSection>
{/* Filtres étendus */}
<div className="mt-4 border-t border-[var(--border)]/50 pt-4">
@@ -331,70 +285,12 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
</div>
{/* Résumé des filtres actifs */}
{activeFiltersCount > 0 && (
<div className="bg-[var(--card)]/30 rounded-lg p-3 border border-[var(--border)]/50 mt-4">
<div className="text-xs text-[var(--muted-foreground)] font-mono uppercase tracking-wider mb-2">
Filtres actifs
</div>
<div className="space-y-1 text-xs">
{filters.search && (
<div className="text-[var(--muted-foreground)]">
Recherche: <span className="text-cyan-400">&ldquo;{filters.search}&rdquo;</span>
</div>
)}
{(filters.priorities?.filter(Boolean).length || 0) > 0 && (
<div className="text-[var(--muted-foreground)]">
Priorités: <span className="text-cyan-400">{filters.priorities?.filter(Boolean).join(', ')}</span>
</div>
)}
{(filters.tags?.filter(Boolean).length || 0) > 0 && (
<div className="text-[var(--muted-foreground)]">
Tags: <span className="text-cyan-400">{filters.tags?.filter(Boolean).join(', ')}</span>
</div>
)}
{filters.showWithDueDate && (
<div className="text-[var(--muted-foreground)]">
Affichage: <span className="text-green-400">Avec date de fin</span>
</div>
)}
{filters.showJiraOnly && (
<div className="text-[var(--muted-foreground)]">
Affichage: <span className="text-blue-400">Jira seulement</span>
</div>
)}
{filters.hideJiraTasks && (
<div className="text-[var(--muted-foreground)]">
Affichage: <span className="text-red-400">Masquer Jira</span>
</div>
)}
{(filters.jiraProjects?.filter(Boolean).length || 0) > 0 && (
<div className="text-[var(--muted-foreground)]">
Projets Jira: <span className="text-blue-400">{filters.jiraProjects?.filter(Boolean).join(', ')}</span>
</div>
)}
{(filters.jiraTypes?.filter(Boolean).length || 0) > 0 && (
<div className="text-[var(--muted-foreground)]">
Types Jira: <span className="text-purple-400">{filters.jiraTypes?.filter(Boolean).join(', ')}</span>
</div>
)}
{filters.showTfsOnly && (
<div className="text-[var(--muted-foreground)]">
Affichage: <span className="text-orange-400">TFS seulement</span>
</div>
)}
{filters.hideTfsTasks && (
<div className="text-[var(--muted-foreground)]">
Affichage: <span className="text-red-400">Masquer TFS</span>
</div>
)}
{(filters.tfsProjects?.filter(Boolean).length || 0) > 0 && (
<div className="text-[var(--muted-foreground)]">
Projets TFS: <span className="text-orange-400">{filters.tfsProjects?.filter(Boolean).join(', ')}</span>
</div>
)}
</div>
</div>
)}
<FilterSummary
filters={filters}
activeFiltersCount={activeFiltersCount}
onClearFilters={handleClearFilters}
className="mt-6"
/>
</div>
</div>
@@ -464,6 +360,6 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
</div>,
document.body
)}
</div>
</ControlPanel>
);
}

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { Button, ToggleButton, ControlPanel } from '@/components/ui';
import { SourceQuickFilter } from '@/components/kanban/SourceQuickFilter';
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
import type { KanbanFilters } from '@/lib/types';
@@ -34,102 +34,96 @@ export function MobileControls({
const [isMenuOpen, setIsMenuOpen] = useState(false);
return (
<div className="bg-[var(--card)]/30 border-b border-[var(--border)]/30">
<div className="px-4 py-2">
{/* Barre principale mobile */}
<div className="flex items-center justify-between">
{/* Bouton menu hamburger */}
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="flex items-center gap-2 px-3 py-2 rounded-md bg-[var(--card)] border border-[var(--border)] text-[var(--muted-foreground)] hover:border-[var(--primary)]/50 transition-all"
>
<ControlPanel className="px-4 py-2">
{/* Barre principale mobile */}
<div className="flex items-center justify-between">
{/* Bouton menu hamburger */}
<ToggleButton
variant="primary"
isActive={isMenuOpen}
count={activeFiltersCount}
onClick={() => setIsMenuOpen(!isMenuOpen)}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
<span className="text-sm font-mono">Options</span>
{activeFiltersCount > 0 && (
<span className="bg-[var(--primary)]/20 text-[var(--primary)] text-xs px-1.5 py-0.5 rounded-full font-mono">
{activeFiltersCount}
</span>
)}
</button>
}
>
Options
</ToggleButton>
{/* Bouton d'ajout de tâche */}
<Button
variant="primary"
onClick={onCreateTask}
className="flex items-center gap-2"
size="sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
<span className="hidden xs:inline">Nouvelle</span>
</Button>
</div>
{/* Bouton d'ajout de tâche */}
<Button
variant="primary"
onClick={onCreateTask}
className="flex items-center gap-2"
size="sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
<span className="hidden xs:inline">Nouvelle</span>
</Button>
</div>
{/* Menu déroulant */}
{isMenuOpen && (
<div className="mt-3 p-3 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg">
{/* Section Affichage */}
<div className="mb-4">
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
Affichage
</h3>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => {
onToggleFilters();
setIsMenuOpen(false);
}}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono transition-all ${
showFilters
? 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30'
: 'bg-[var(--muted)]/30 text-[var(--muted-foreground)] border border-[var(--border)]'
}`}
>
{/* Menu déroulant */}
{isMenuOpen && (
<div className="mt-3 p-3 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg">
{/* Section Affichage */}
<div className="mb-4">
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
Affichage
</h3>
<div className="grid grid-cols-2 gap-2">
<ToggleButton
variant="primary"
isActive={showFilters}
onClick={() => {
onToggleFilters();
setIsMenuOpen(false);
}}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
</svg>
Filtres
</button>
<button
onClick={() => {
onToggleObjectives();
setIsMenuOpen(false);
}}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono transition-all ${
showObjectives
? 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30'
: 'bg-[var(--muted)]/30 text-[var(--muted-foreground)] border border-[var(--border)]'
}`}
>
}
>
Filtres
</ToggleButton>
<ToggleButton
variant="accent"
isActive={showObjectives}
onClick={() => {
onToggleObjectives();
setIsMenuOpen(false);
}}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
Objectifs
</button>
</div>
}
>
Objectifs
</ToggleButton>
</div>
</div>
{/* Section Paramètres */}
<div className="mb-4">
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
Paramètres
</h3>
<div className="space-y-2">
<button
onClick={() => {
onToggleCompactView();
setIsMenuOpen(false);
}}
className={`w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono transition-all ${
compactView
? 'bg-[var(--secondary)]/20 text-[var(--secondary)] border border-[var(--secondary)]/30'
: 'bg-[var(--muted)]/30 text-[var(--muted-foreground)] border border-[var(--border)]'
}`}
>
{/* Section Paramètres */}
<div className="mb-4">
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
Paramètres
</h3>
<div className="space-y-2">
<ToggleButton
variant="secondary"
isActive={compactView}
onClick={() => {
onToggleCompactView();
setIsMenuOpen(false);
}}
className="w-full"
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{compactView ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
@@ -137,31 +131,32 @@ export function MobileControls({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
Vue {compactView ? 'détaillée' : 'compacte'}
</button>
}
>
Vue {compactView ? 'détaillée' : 'compacte'}
</ToggleButton>
<div className="flex items-center justify-between px-3 py-2 bg-[var(--muted)]/30 border border-[var(--border)] rounded-md">
<span className="text-sm font-mono text-[var(--muted-foreground)]">Taille police</span>
<FontSizeToggle />
</div>
</div>
</div>
{/* Section Sources */}
<div>
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
Sources
</h3>
<div className="space-y-2">
<SourceQuickFilter
filters={kanbanFilters}
onFiltersChange={onFiltersChange}
/>
<div className="flex items-center justify-between px-3 py-2 bg-[var(--muted)]/30 border border-[var(--border)] rounded-md">
<span className="text-sm font-mono text-[var(--muted-foreground)]">Taille police</span>
<FontSizeToggle />
</div>
</div>
</div>
)}
</div>
</div>
{/* Section Sources */}
<div>
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
Sources
</h3>
<div className="space-y-2">
<SourceQuickFilter
filters={kanbanFilters}
onFiltersChange={onFiltersChange}
/>
</div>
</div>
</div>
)}
</ControlPanel>
);
}

View File

@@ -128,7 +128,7 @@ export function ObjectivesBoard({
<div className="bg-[var(--card)]/30 border-b border-[var(--accent)]/30">
<div className="container mx-auto px-6 py-4">
<Card variant="column" className="border-[var(--accent)]/30 shadow-[var(--accent)]/10">
<CardHeader className="pb-3">
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<button
onClick={toggleObjectivesCollapse}
@@ -172,7 +172,7 @@ export function ObjectivesBoard({
</CardHeader>
{!isCollapsed && (
<CardContent className="pt-0">
<CardContent className="pt-3">
{(() => {
// Séparer les tâches par statut
const inProgressTasks = tasks.filter(task => task.status === 'in_progress');

View File

@@ -2,6 +2,7 @@
import { TaskStatus, Task } from '@/lib/types';
import { getAllStatuses } from '@/lib/status-config';
import { FilterChip } from '@/components/ui';
interface ColumnFiltersProps {
hiddenStatuses: Set<TaskStatus>;
@@ -19,18 +20,18 @@ export function ColumnFilters({ hiddenStatuses, onToggleStatus, tasks }: ColumnF
{getAllStatuses().map(statusConfig => {
const statusCount = tasks.filter(task => task.status === statusConfig.key).length;
return (
<button
<FilterChip
key={statusConfig.key}
onClick={() => onToggleStatus(statusConfig.key)}
className={`px-2 py-1 rounded border text-xs font-medium transition-colors ${
hiddenStatuses.has(statusConfig.key)
? 'bg-[var(--muted)]/20 text-[var(--muted)] border-[var(--muted)]/30 hover:bg-[var(--muted)]/30'
: 'bg-[var(--primary)]/20 text-[var(--primary)] border-[var(--primary)]/30 hover:bg-[var(--primary)]/30'
}`}
variant={hiddenStatuses.has(statusConfig.key) ? 'hidden' : 'default'}
count={statusCount}
icon={
hiddenStatuses.has(statusConfig.key) ? '👁️‍🗨️' : '👁️'
}
title={hiddenStatuses.has(statusConfig.key) ? `Afficher ${statusConfig.label}` : `Masquer ${statusConfig.label}`}
>
{hiddenStatuses.has(statusConfig.key) ? '👁️‍🗨️' : '👁️'} {statusConfig.label}{statusCount ? ` (${statusCount})` : ''}
</button>
{statusConfig.label}
</FilterChip>
);
})}
</div>

View File

@@ -1,5 +1,7 @@
'use client';
import { FilterChip } from '@/components/ui';
interface GeneralFiltersProps {
showWithDueDate?: boolean;
onDueDateFilterToggle: () => void;
@@ -12,25 +14,22 @@ export function GeneralFilters({ showWithDueDate = false, onDueDateFilterToggle
Généraux
</label>
<div className="flex flex-wrap gap-1">
<button
type="button"
<FilterChip
onClick={onDueDateFilterToggle}
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium cursor-pointer ${
showWithDueDate
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)] hover:bg-[var(--card)]/80'
}`}
variant={showWithDueDate ? 'selected' : 'default'}
icon={
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 002 2v12a2 2 0 002 2z" />
</svg>
}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 002 2v12a2 2 0 002 2z" />
</svg>
Avec date de fin
</button>
</FilterChip>
</div>
</div>
);

View File

@@ -1,7 +1,7 @@
'use client';
import { useMemo } from 'react';
import { Button } from '@/components/ui/Button';
import { Button, FilterChip } from '@/components/ui';
import { useTasksContext } from '@/contexts/TasksContext';
import type { KanbanFilters } from '@/lib/types';
@@ -160,17 +160,15 @@ export function JiraFilters({ filters, onFiltersChange }: JiraFiltersProps) {
</label>
<div className="flex flex-wrap gap-1">
{availableJiraProjects.map((project) => (
<button
<FilterChip
key={project}
onClick={() => handleJiraProjectToggle(project)}
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.jiraProjects?.includes(project)
? 'border-blue-400 bg-blue-400/10 text-blue-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
variant={filters.jiraProjects?.includes(project) ? 'selected' : 'default'}
count={jiraProjectCounts[project]}
icon="📋"
>
📋 {project} ({jiraProjectCounts[project]})
</button>
{project}
</FilterChip>
))}
</div>
</div>
@@ -183,25 +181,31 @@ export function JiraFilters({ filters, onFiltersChange }: JiraFiltersProps) {
Types
</label>
<div className="flex flex-wrap gap-1">
{availableJiraTypes.map((type) => (
<button
key={type}
onClick={() => handleJiraTypeToggle(type)}
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.jiraTypes?.includes(type)
? 'border-purple-400 bg-purple-400/10 text-purple-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
{type === 'Feature' && '✨ '}
{type === 'Story' && '📖 '}
{type === 'Task' && '📝 '}
{type === 'Bug' && '🐛 '}
{type === 'Support' && '🛠️ '}
{type === 'Enabler' && '🔧 '}
{type} ({jiraTypeCounts[type]})
</button>
))}
{availableJiraTypes.map((type) => {
const getTypeIcon = (type: string) => {
switch (type) {
case 'Feature': return '✨';
case 'Story': return '📖';
case 'Task': return '📝';
case 'Bug': return '🐛';
case 'Support': return '🛠️';
case 'Enabler': return '🔧';
default: return '📋';
}
};
return (
<FilterChip
key={type}
onClick={() => handleJiraTypeToggle(type)}
variant={filters.jiraTypes?.includes(type) ? 'selected' : 'default'}
count={jiraTypeCounts[type]}
icon={getTypeIcon(type)}
>
{type}
</FilterChip>
);
})}
</div>
</div>
)}

View File

@@ -4,6 +4,7 @@ import { useMemo } from 'react';
import { TaskPriority } from '@/lib/types';
import { useTasksContext } from '@/contexts/TasksContext';
import { getAllPriorities, getPriorityColorHex } from '@/lib/status-config';
import { FilterChip } from '@/components/ui';
interface PriorityFiltersProps {
selectedPriorities?: TaskPriority[];
@@ -44,22 +45,16 @@ export function PriorityFilters({ selectedPriorities = [], onPriorityToggle }: P
) : (
<div className="flex flex-wrap gap-1">
{visiblePriorities.map((priority) => (
<button
key={priority.value}
onClick={() => onPriorityToggle(priority.value)}
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium ${
selectedPriorities.includes(priority.value)
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: getPriorityColorHex(priority.color) }}
/>
{priority.label} ({priority.count})
</button>
))}
<FilterChip
key={priority.value}
onClick={() => onPriorityToggle(priority.value)}
variant={selectedPriorities.includes(priority.value) ? 'selected' : 'priority'}
color={getPriorityColorHex(priority.color)}
count={priority.count}
>
{priority.label}
</FilterChip>
))}
</div>
)}
</div>

View File

@@ -2,6 +2,7 @@
import { useMemo } from 'react';
import { useTasksContext } from '@/contexts/TasksContext';
import { FilterChip } from '@/components/ui';
interface TagFiltersProps {
selectedTags?: string[];
@@ -48,22 +49,16 @@ export function TagFilters({ selectedTags = [], onTagToggle }: TagFiltersProps)
) : (
<div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto">
{visibleTags.map((tag) => (
<button
key={tag.id}
onClick={() => onTagToggle(tag.name)}
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium ${
selectedTags.includes(tag.name)
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: tag.color }}
/>
{tag.name} ({tagCounts[tag.name]})
</button>
))}
<FilterChip
key={tag.id}
onClick={() => onTagToggle(tag.name)}
variant={selectedTags.includes(tag.name) ? 'selected' : 'tag'}
color={tag.color}
count={tagCounts[tag.name]}
>
{tag.name}
</FilterChip>
))}
</div>
)}
</div>

View File

@@ -1,7 +1,7 @@
'use client';
import { useMemo } from 'react';
import { Button } from '@/components/ui/Button';
import { Button, FilterChip } from '@/components/ui';
import { useTasksContext } from '@/contexts/TasksContext';
import type { KanbanFilters } from '@/lib/types';
@@ -125,17 +125,15 @@ export function TfsFilters({ filters, onFiltersChange }: TfsFiltersProps) {
</label>
<div className="flex flex-wrap gap-1">
{availableTfsProjects.map((project) => (
<button
<FilterChip
key={project}
onClick={() => handleTfsProjectToggle(project)}
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.tfsProjects?.includes(project)
? 'border-orange-400 bg-orange-400/10 text-orange-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
variant={filters.tfsProjects?.includes(project) ? 'selected' : 'default'}
count={tfsProjectCounts[project]}
icon="📦"
>
📦 {project} ({tfsProjectCounts[project]})
</button>
{project}
</FilterChip>
))}
</div>
</div>

View File

@@ -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 } from '@/components/ui';
import { StatCard, ProgressBar, ActionCard, TaskCard, MetricCard, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup, FilterSummary, FilterChip } from '@/components/ui';
import { ThemeSelector } from '@/components/ThemeSelector';
export function UIShowcaseClient() {
@@ -562,6 +562,243 @@ export function UIShowcaseClient() {
</div>
</section>
{/* Kanban Components Section */}
<section className="space-y-8">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
Kanban Components
</h2>
{/* Toggle Buttons */}
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">Toggle Buttons</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">
variant="primary" isActive={true}
</div>
<ToggleButton
variant="primary"
isActive={true}
count={3}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
</svg>
}
>
Filtres
</ToggleButton>
</div>
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
variant="primary" isActive={true} - Icône seule (padding réduit)
</div>
<ToggleButton
variant="primary"
isActive={true}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
</svg>
}
/>
</div>
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
variant="accent" isActive={false}
</div>
<ToggleButton
variant="accent"
isActive={false}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
}
>
Objectifs
</ToggleButton>
</div>
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
variant="warning" isActive={true}
</div>
<ToggleButton
variant="warning"
isActive={true}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
</svg>
}
>
Swimlanes
</ToggleButton>
</div>
</div>
</div>
{/* Search Input */}
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">Search Input</h3>
<div className="space-y-4 max-w-md">
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
placeholder="Rechercher des tâches..."
</div>
<SearchInput
placeholder="Rechercher des tâches..."
onChange={(value) => console.log('Search:', value)}
/>
</div>
</div>
</div>
{/* Control Panel */}
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">Control Panel</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">
ControlPanel + ControlSection + ControlGroup
</div>
<ControlPanel>
<ControlSection>
<SearchInput
placeholder="Rechercher..."
onChange={(value) => console.log('Search:', value)}
/>
<ControlGroup>
<ToggleButton
variant="primary"
isActive={true}
count={2}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
</svg>
}
>
Filtres
</ToggleButton>
<ToggleButton
variant="accent"
isActive={false}
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
}
>
Objectifs
</ToggleButton>
</ControlGroup>
</ControlSection>
</ControlPanel>
</div>
</div>
</div>
{/* Filter Summary */}
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">Filter Summary</h3>
<div className="space-y-4 max-w-2xl">
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
Exemple avec filtres actifs
</div>
<FilterSummary
filters={{
search: "refactorisation",
priorities: ["Haute", "Moyenne"],
tags: ["Frontend", "UI"],
showWithDueDate: true,
showJiraOnly: true,
jiraProjects: ["PROJ-1", "PROJ-2"],
jiraTypes: ["Bug", "Story"]
}}
activeFiltersCount={7}
onClearFilters={() => console.log('Clear filters')}
/>
</div>
</div>
</div>
{/* Filter Chips */}
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">Filter Chips</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">
variant="default"
</div>
<div className="flex flex-wrap gap-2">
<FilterChip variant="default" onClick={() => console.log('Default chip')}>
Filtre par défaut
</FilterChip>
<FilterChip variant="default" count={5} onClick={() => console.log('With count')}>
Avec compteur
</FilterChip>
<FilterChip variant="default" icon="🏷️" onClick={() => console.log('With icon')}>
Avec icône
</FilterChip>
</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">
variant="selected"
</div>
<div className="flex flex-wrap gap-2">
<FilterChip variant="selected" onClick={() => console.log('Selected chip')}>
Filtre sélectionné
</FilterChip>
<FilterChip variant="selected" count={3} color="#8b5cf6" onClick={() => console.log('Selected with color')}>
Avec couleur
</FilterChip>
</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">
variant="hidden"
</div>
<div className="flex flex-wrap gap-2">
<FilterChip variant="hidden" icon="👁️‍🗨️" onClick={() => console.log('Hidden chip')}>
Colonne masquée
</FilterChip>
<FilterChip variant="hidden" count={0} icon="👁️‍🗨️" onClick={() => console.log('Hidden empty')}>
Colonne vide
</FilterChip>
</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">
Exemples avec icônes (Jira/TFS)
</div>
<div className="flex flex-wrap gap-2">
<FilterChip variant="selected" icon="📋" count={5} onClick={() => console.log('Jira project')}>
PROJ-123
</FilterChip>
<FilterChip variant="selected" icon="🐛" count={2} onClick={() => console.log('Bug type')}>
Bug
</FilterChip>
<FilterChip variant="default" icon="📦" count={3} onClick={() => console.log('TFS project')}>
TFS-Projet
</FilterChip>
<FilterChip variant="selected" icon="✨" count={1} onClick={() => console.log('Feature type')}>
Feature
</FilterChip>
</div>
</div>
</div>
</div>
</section>
{/* Footer */}
<div className="text-center pt-8 border-t border-[var(--border)]">
<p className="text-[var(--muted-foreground)]">

View 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>
);
}

View 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 };

View 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>
);
}

View 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 };

View 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 };

View File

@@ -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';