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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
<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..."
|
||||
/>
|
||||
|
||||
<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'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
}
|
||||
>
|
||||
Filtres
|
||||
</ToggleButton>
|
||||
|
||||
<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'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
}
|
||||
>
|
||||
Objectifs
|
||||
</ToggleButton>
|
||||
|
||||
<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"}
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
{/* 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}
|
||||
/>
|
||||
|
||||
<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"}
|
||||
>
|
||||
<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>
|
||||
}
|
||||
>
|
||||
{compactView ? 'Détaillée' : 'Compacte'}
|
||||
</ToggleButton>
|
||||
|
||||
<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"}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">“{filters.search}”</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
>
|
||||
Filtres
|
||||
</ToggleButton>
|
||||
|
||||
<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)]'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)]">
|
||||
|
||||
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