From 0fcd4d68c1f7c95e9d0198d0ac9b8513e5324e62 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 28 Sep 2025 21:53:22 +0200 Subject: [PATCH] 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. --- src/components/jira/JiraLogs.tsx | 2 +- src/components/jira/JiraSync.tsx | 2 +- src/components/kanban/Column.tsx | 2 +- src/components/kanban/DesktopControls.tsx | 198 +++++++-------- src/components/kanban/KanbanFilters.tsx | 178 +++---------- src/components/kanban/MobileControls.tsx | 207 ++++++++------- src/components/kanban/ObjectivesBoard.tsx | 4 +- .../kanban/filters/ColumnFilters.tsx | 17 +- .../kanban/filters/GeneralFilters.tsx | 31 ++- src/components/kanban/filters/JiraFilters.tsx | 60 +++-- .../kanban/filters/PriorityFilters.tsx | 27 +- src/components/kanban/filters/TagFilters.tsx | 27 +- src/components/kanban/filters/TfsFilters.tsx | 16 +- .../ui-showcase/UIShowcaseClient.tsx | 239 +++++++++++++++++- src/components/ui/ControlPanel.tsx | 46 ++++ src/components/ui/FilterChip.tsx | 75 ++++++ src/components/ui/FilterSummary.tsx | 168 ++++++++++++ src/components/ui/SearchInput.tsx | 78 ++++++ src/components/ui/ToggleButton.tsx | 78 ++++++ src/components/ui/index.ts | 7 + 20 files changed, 1011 insertions(+), 451 deletions(-) create mode 100644 src/components/ui/ControlPanel.tsx create mode 100644 src/components/ui/FilterChip.tsx create mode 100644 src/components/ui/FilterSummary.tsx create mode 100644 src/components/ui/SearchInput.tsx create mode 100644 src/components/ui/ToggleButton.tsx diff --git a/src/components/jira/JiraLogs.tsx b/src/components/jira/JiraLogs.tsx index 5cef6d4..2b46758 100644 --- a/src/components/jira/JiraLogs.tsx +++ b/src/components/jira/JiraLogs.tsx @@ -62,7 +62,7 @@ export function JiraLogs({ className = "" }: JiraLogsProps) { return ( - +
diff --git a/src/components/jira/JiraSync.tsx b/src/components/jira/JiraSync.tsx index cfaaffd..cccece0 100644 --- a/src/components/jira/JiraSync.tsx +++ b/src/components/jira/JiraSync.tsx @@ -147,7 +147,7 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) { return ( - +
diff --git a/src/components/kanban/Column.tsx b/src/components/kanban/Column.tsx index faeccc1..6df83a9 100644 --- a/src/components/kanban/Column.tsx +++ b/src/components/kanban/Column.tsx @@ -38,7 +38,7 @@ export function KanbanColumn({ id, tasks, onCreateTask, onEditTask, compactView isOver ? 'ring-2 ring-[var(--primary)]/50 bg-[var(--card-hover)]' : '' }`} > - +
diff --git a/src/components/kanban/DesktopControls.tsx b/src/components/kanban/DesktopControls.tsx index 849e6ec..5c13ec3 100644 --- a/src/components/kanban/DesktopControls.tsx +++ b/src/components/kanban/DesktopControls.tsx @@ -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 ( -
-
- {/* Layout responsive : deux lignes sur tablette, une ligne sur desktop */} -
- {/* Section gauche : Recherche + Boutons principaux */} -
- {/* Champ de recherche */} -
- handleSearchChange(e.target.value)} - placeholder="Rechercher des tâches..." - className="bg-[var(--card)] border-[var(--border)] w-full" - /> -
- -
- - - - - -
-
+ } + /> + + - {/* Section droite : Raccourcis + Bouton Nouvelle tâche */} -
-
- {/* Raccourcis Sources (Jira & TFS) */} - - - - - - - {/* Font Size Toggle */} - -
- - {/* Bouton d'ajout de tâche */} - -
-
+ {swimlanesByTags ? 'Standard' : 'Swimlanes'} + + + {/* Font Size Toggle */} + + + + {/* Bouton d'ajout de tâche */} + +
-
+ ); } diff --git a/src/components/kanban/KanbanFilters.tsx b/src/components/kanban/KanbanFilters.tsx index 89962f4..ed4d19d 100644 --- a/src/components/kanban/KanbanFilters.tsx +++ b/src/components/kanban/KanbanFilters.tsx @@ -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(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 ( -
+
{/* Header avec recherche et bouton expand */} -
+
- handleSearchChange(e.target.value)} +
{/* Menu swimlanes - masqué sur mobile */} {!isMobile && ( -
- + {/* Bouton pour changer le mode des swimlanes */} {filters.swimlanesByTags && ( @@ -240,7 +203,7 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH )} -
+ )} @@ -273,16 +236,7 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
- {activeFiltersCount > 0 && ( - - )} -
+ {/* Filtres étendus */}
@@ -331,70 +285,12 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
{/* Résumé des filtres actifs */} - {activeFiltersCount > 0 && ( -
-
- Filtres actifs -
-
- {filters.search && ( -
- Recherche: “{filters.search}” -
- )} - {(filters.priorities?.filter(Boolean).length || 0) > 0 && ( -
- Priorités: {filters.priorities?.filter(Boolean).join(', ')} -
- )} - {(filters.tags?.filter(Boolean).length || 0) > 0 && ( -
- Tags: {filters.tags?.filter(Boolean).join(', ')} -
- )} - {filters.showWithDueDate && ( -
- Affichage: Avec date de fin -
- )} - {filters.showJiraOnly && ( -
- Affichage: Jira seulement -
- )} - {filters.hideJiraTasks && ( -
- Affichage: Masquer Jira -
- )} - {(filters.jiraProjects?.filter(Boolean).length || 0) > 0 && ( -
- Projets Jira: {filters.jiraProjects?.filter(Boolean).join(', ')} -
- )} - {(filters.jiraTypes?.filter(Boolean).length || 0) > 0 && ( -
- Types Jira: {filters.jiraTypes?.filter(Boolean).join(', ')} -
- )} - {filters.showTfsOnly && ( -
- Affichage: TFS seulement -
- )} - {filters.hideTfsTasks && ( -
- Affichage: Masquer TFS -
- )} - {(filters.tfsProjects?.filter(Boolean).length || 0) > 0 && ( -
- Projets TFS: {filters.tfsProjects?.filter(Boolean).join(', ')} -
- )} -
-
- )} +
@@ -464,6 +360,6 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
, document.body )} -
+ ); } diff --git a/src/components/kanban/MobileControls.tsx b/src/components/kanban/MobileControls.tsx index eb2f6d3..72b3782 100644 --- a/src/components/kanban/MobileControls.tsx +++ b/src/components/kanban/MobileControls.tsx @@ -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 ( -
-
- {/* Barre principale mobile */} -
- {/* Bouton menu hamburger */} - + } + > + Options + - {/* Bouton d'ajout de tâche */} - -
+ {/* Bouton d'ajout de tâche */} + +
- {/* Menu déroulant */} - {isMenuOpen && ( -
- {/* Section Affichage */} -
-

- Affichage -

-
- - - -
+ } + > + Objectifs +
+
- {/* Section Paramètres */} -
-

- Paramètres -

-
- + } + > + Vue {compactView ? 'détaillée' : 'compacte'} + -
- Taille police - -
-
-
- - {/* Section Sources */} -
-

- Sources -

-
- +
+ Taille police +
- )} -
-
+ + {/* Section Sources */} +
+

+ Sources +

+
+ +
+
+
+ )} + ); } \ No newline at end of file diff --git a/src/components/kanban/ObjectivesBoard.tsx b/src/components/kanban/ObjectivesBoard.tsx index 5c28a4e..10a6644 100644 --- a/src/components/kanban/ObjectivesBoard.tsx +++ b/src/components/kanban/ObjectivesBoard.tsx @@ -128,7 +128,7 @@ export function ObjectivesBoard({
- +
+ {statusConfig.label} + ); })}
diff --git a/src/components/kanban/filters/GeneralFilters.tsx b/src/components/kanban/filters/GeneralFilters.tsx index 98532d6..c64a92b 100644 --- a/src/components/kanban/filters/GeneralFilters.tsx +++ b/src/components/kanban/filters/GeneralFilters.tsx @@ -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
- +
); diff --git a/src/components/kanban/filters/JiraFilters.tsx b/src/components/kanban/filters/JiraFilters.tsx index b088436..9cea8e0 100644 --- a/src/components/kanban/filters/JiraFilters.tsx +++ b/src/components/kanban/filters/JiraFilters.tsx @@ -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) {
{availableJiraProjects.map((project) => ( - + {project} + ))}
@@ -183,25 +181,31 @@ export function JiraFilters({ filters, onFiltersChange }: JiraFiltersProps) { Types
- {availableJiraTypes.map((type) => ( - - ))} + {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 ( + handleJiraTypeToggle(type)} + variant={filters.jiraTypes?.includes(type) ? 'selected' : 'default'} + count={jiraTypeCounts[type]} + icon={getTypeIcon(type)} + > + {type} + + ); + })}
)} diff --git a/src/components/kanban/filters/PriorityFilters.tsx b/src/components/kanban/filters/PriorityFilters.tsx index acf3943..efa854e 100644 --- a/src/components/kanban/filters/PriorityFilters.tsx +++ b/src/components/kanban/filters/PriorityFilters.tsx @@ -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 ) : (
{visiblePriorities.map((priority) => ( - - ))} + onPriorityToggle(priority.value)} + variant={selectedPriorities.includes(priority.value) ? 'selected' : 'priority'} + color={getPriorityColorHex(priority.color)} + count={priority.count} + > + {priority.label} + + ))}
)} diff --git a/src/components/kanban/filters/TagFilters.tsx b/src/components/kanban/filters/TagFilters.tsx index 3c6890d..d24bf59 100644 --- a/src/components/kanban/filters/TagFilters.tsx +++ b/src/components/kanban/filters/TagFilters.tsx @@ -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) ) : (
{visibleTags.map((tag) => ( - - ))} + onTagToggle(tag.name)} + variant={selectedTags.includes(tag.name) ? 'selected' : 'tag'} + color={tag.color} + count={tagCounts[tag.name]} + > + {tag.name} + + ))}
)} diff --git a/src/components/kanban/filters/TfsFilters.tsx b/src/components/kanban/filters/TfsFilters.tsx index 9b0d7d0..d61f248 100644 --- a/src/components/kanban/filters/TfsFilters.tsx +++ b/src/components/kanban/filters/TfsFilters.tsx @@ -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) {
{availableTfsProjects.map((project) => ( - + {project} + ))}
diff --git a/src/components/ui-showcase/UIShowcaseClient.tsx b/src/components/ui-showcase/UIShowcaseClient.tsx index a5d1066..f56c5ad 100644 --- a/src/components/ui-showcase/UIShowcaseClient.tsx +++ b/src/components/ui-showcase/UIShowcaseClient.tsx @@ -8,7 +8,7 @@ import { Alert, AlertTitle, AlertDescription } from '@/components/ui/Alert'; import { Input } from '@/components/ui/Input'; import { StyledCard } from '@/components/ui/StyledCard'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; -import { StatCard, ProgressBar, ActionCard, TaskCard, MetricCard } 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() { + {/* Kanban Components Section */} +
+

+ Kanban Components +

+ + {/* Toggle Buttons */} +
+

Toggle Buttons

+
+
+
+ variant="primary" isActive={true} +
+ + + + } + > + Filtres + +
+ +
+
+ variant="primary" isActive={true} - Icône seule (padding réduit) +
+ + + + } + /> +
+ +
+
+ variant="accent" isActive={false} +
+ + + + } + > + Objectifs + +
+ +
+
+ variant="warning" isActive={true} +
+ + + + } + > + Swimlanes + +
+
+
+ + {/* Search Input */} +
+

Search Input

+
+
+
+ placeholder="Rechercher des tâches..." +
+ console.log('Search:', value)} + /> +
+
+
+ + {/* Control Panel */} +
+

Control Panel

+
+
+
+ ControlPanel + ControlSection + ControlGroup +
+ + + console.log('Search:', value)} + /> + + + + + } + > + Filtres + + + + + } + > + Objectifs + + + + +
+
+
+ + {/* Filter Summary */} +
+

Filter Summary

+
+
+
+ Exemple avec filtres actifs +
+ console.log('Clear filters')} + /> +
+
+
+ + {/* Filter Chips */} +
+

Filter Chips

+
+
+
+ variant="default" +
+
+ console.log('Default chip')}> + Filtre par défaut + + console.log('With count')}> + Avec compteur + + console.log('With icon')}> + Avec icône + +
+
+ +
+
+ variant="selected" +
+
+ console.log('Selected chip')}> + Filtre sélectionné + + console.log('Selected with color')}> + Avec couleur + +
+
+ +
+
+ variant="hidden" +
+
+ console.log('Hidden chip')}> + Colonne masquée + + console.log('Hidden empty')}> + Colonne vide + +
+
+ +
+
+ Exemples avec icônes (Jira/TFS) +
+
+ console.log('Jira project')}> + PROJ-123 + + console.log('Bug type')}> + Bug + + console.log('TFS project')}> + TFS-Projet + + console.log('Feature type')}> + Feature + +
+
+
+
+
+ {/* Footer */}

diff --git a/src/components/ui/ControlPanel.tsx b/src/components/ui/ControlPanel.tsx new file mode 100644 index 0000000..144d9d2 --- /dev/null +++ b/src/components/ui/ControlPanel.tsx @@ -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 ( +

+
+ {children} +
+
+ ); +} + +interface ControlSectionProps { + children: ReactNode; + className?: string; +} + +export function ControlSection({ children, className }: ControlSectionProps) { + return ( +
+ {children} +
+ ); +} + +interface ControlGroupProps { + children: ReactNode; + className?: string; +} + +export function ControlGroup({ children, className }: ControlGroupProps) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/ui/FilterChip.tsx b/src/components/ui/FilterChip.tsx new file mode 100644 index 0000000..67c9d27 --- /dev/null +++ b/src/components/ui/FilterChip.tsx @@ -0,0 +1,75 @@ +import { ButtonHTMLAttributes, forwardRef } from 'react'; +import { cn } from '@/lib/utils'; + +interface FilterChipProps extends ButtonHTMLAttributes { + variant?: 'default' | 'selected' | 'hidden' | 'priority' | 'tag'; + color?: string; + count?: number; + icon?: React.ReactNode; + size?: 'sm' | 'md'; +} + +const FilterChip = forwardRef( + ({ + 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 ( + + ); + } +); + +FilterChip.displayName = 'FilterChip'; + +export { FilterChip }; diff --git a/src/components/ui/FilterSummary.tsx b/src/components/ui/FilterSummary.tsx new file mode 100644 index 0000000..a3eceb6 --- /dev/null +++ b/src/components/ui/FilterSummary.tsx @@ -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 ( + + +
+ + Filtres actifs ({activeFiltersCount}) + + {onClearFilters && ( + + )} +
+
+ +
+ {filterItems.map((item, index) => ( +
+ + {item.label}: + + + {item.value} + +
+ ))} +
+
+
+ ); +} diff --git a/src/components/ui/SearchInput.tsx b/src/components/ui/SearchInput.tsx new file mode 100644 index 0000000..b1f1849 --- /dev/null +++ b/src/components/ui/SearchInput.tsx @@ -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, 'onChange'> { + value?: string; + onChange?: (value: string) => void; + onDebouncedChange?: (value: string) => void; + debounceMs?: number; + placeholder?: string; + className?: string; +} + +const SearchInput = forwardRef( + ({ + value = '', + onChange, + onDebouncedChange, + debounceMs = 300, + placeholder = "Rechercher...", + className, + ...props + }, ref) => { + const [localValue, setLocalValue] = useState(value); + const timeoutRef = useRef(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) => { + 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 ( +
+ +
+ ); + } +); + +SearchInput.displayName = 'SearchInput'; + +export { SearchInput }; diff --git a/src/components/ui/ToggleButton.tsx b/src/components/ui/ToggleButton.tsx new file mode 100644 index 0000000..bd7ba13 --- /dev/null +++ b/src/components/ui/ToggleButton.tsx @@ -0,0 +1,78 @@ +import { ButtonHTMLAttributes, forwardRef } from 'react'; +import { cn } from '@/lib/utils'; + +interface ToggleButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'accent' | 'secondary' | 'warning' | 'cyan'; + size?: 'sm' | 'md'; + isActive?: boolean; + icon?: React.ReactNode; + count?: number; +} + +const ToggleButton = forwardRef( + ({ 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 ( + + ); + } +); + +ToggleButton.displayName = 'ToggleButton'; + +export { ToggleButton }; diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 4adc60b..b69b5b4 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -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';