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 (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-2 h-2 rounded-full bg-gray-400 animate-pulse"></div>
|
<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 (
|
return (
|
||||||
<Card className={`${className}`}>
|
<Card className={`${className}`}>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-2 h-2 rounded-full animate-pulse" style={{ backgroundColor: 'var(--blue)' }}></div>
|
<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)]' : ''
|
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 justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`w-2 h-2 rounded-full ${style.accent.replace('text-', 'bg-')} animate-pulse`}></div>
|
<div className={`w-2 h-2 rounded-full ${style.accent.replace('text-', 'bg-')} animate-pulse`}></div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup } from '@/components/ui';
|
||||||
import { Input } from '@/components/ui/Input';
|
|
||||||
import { SourceQuickFilter } from '@/components/kanban/SourceQuickFilter';
|
import { SourceQuickFilter } from '@/components/kanban/SourceQuickFilter';
|
||||||
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
|
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
|
||||||
import type { KanbanFilters } from '@/lib/types';
|
import type { KanbanFilters } from '@/lib/types';
|
||||||
@@ -77,86 +76,75 @@ export function DesktopControls({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="bg-[var(--card)]/30 border-b border-[var(--border)]/30 w-full">
|
<ControlPanel>
|
||||||
<div className="w-full px-6 py-2">
|
{/* Layout responsive : deux lignes sur tablette, une ligne sur desktop */}
|
||||||
{/* 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">
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center gap-4 lg:gap-0 w-full">
|
{/* Section gauche : Recherche + Boutons principaux */}
|
||||||
{/* Section gauche : Recherche + Boutons principaux */}
|
<ControlSection>
|
||||||
<div className="flex items-center gap-4">
|
{/* Champ de recherche */}
|
||||||
{/* Champ de recherche */}
|
<SearchInput
|
||||||
<div className="flex-1 min-w-0">
|
value={localSearch}
|
||||||
<Input
|
onChange={handleSearchChange}
|
||||||
type="text"
|
placeholder="Rechercher des tâches..."
|
||||||
value={localSearch}
|
/>
|
||||||
onChange={(e) => handleSearchChange(e.target.value)}
|
|
||||||
placeholder="Rechercher des tâches..."
|
|
||||||
className="bg-[var(--card)] border-[var(--border)] w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<ControlGroup>
|
||||||
<button
|
<ToggleButton
|
||||||
onClick={onToggleFilters}
|
variant="primary"
|
||||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
|
isActive={showFilters}
|
||||||
showFilters
|
count={activeFiltersCount}
|
||||||
? 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30'
|
onClick={onToggleFilters}
|
||||||
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--primary)]/50'
|
icon={
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
Filtres{activeFiltersCount > 0 && ` (${activeFiltersCount})`}
|
}
|
||||||
</button>
|
>
|
||||||
|
Filtres
|
||||||
|
</ToggleButton>
|
||||||
|
|
||||||
<button
|
<ToggleButton
|
||||||
onClick={onToggleObjectives}
|
variant="accent"
|
||||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
|
isActive={showObjectives}
|
||||||
showObjectives
|
onClick={onToggleObjectives}
|
||||||
? 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30'
|
icon={
|
||||||
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--accent)]/50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
Objectifs
|
}
|
||||||
</button>
|
>
|
||||||
|
Objectifs
|
||||||
|
</ToggleButton>
|
||||||
|
|
||||||
<button
|
<ToggleButton
|
||||||
onClick={handleDueDateFilterToggle}
|
variant="cyan"
|
||||||
className={`flex items-center justify-center px-3 py-1.5 rounded-md transition-all mr-4 ${
|
isActive={kanbanFilters.showWithDueDate}
|
||||||
kanbanFilters.showWithDueDate
|
onClick={handleDueDateFilterToggle}
|
||||||
? 'bg-[var(--cyan)]/20 text-[var(--cyan)] border border-[var(--cyan)]/30'
|
title={kanbanFilters.showWithDueDate ? "Afficher toutes les tâches" : "Afficher seulement les tâches avec date de fin"}
|
||||||
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--cyan)]/50'
|
icon={
|
||||||
}`}
|
|
||||||
title={kanbanFilters.showWithDueDate ? "Afficher toutes les tâches" : "Afficher seulement les tâches avec date de fin"}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
||||||
</button>
|
}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</ControlGroup>
|
||||||
|
</ControlSection>
|
||||||
|
|
||||||
{/* Section droite : Raccourcis + Bouton Nouvelle tâche */}
|
{/* Section droite : Raccourcis + Bouton Nouvelle tâche */}
|
||||||
<div className="flex items-center justify-between lg:justify-start gap-4">
|
<ControlSection className="justify-between lg:justify-start">
|
||||||
<div className="flex items-center gap-2 border-l border-[var(--border)] pl-4">
|
<ControlGroup className="border-l border-[var(--border)] ml-2 pl-2 pr-4">
|
||||||
{/* Raccourcis Sources (Jira & TFS) */}
|
{/* Raccourcis Sources (Jira & TFS) */}
|
||||||
<SourceQuickFilter
|
<SourceQuickFilter
|
||||||
filters={kanbanFilters}
|
filters={kanbanFilters}
|
||||||
onFiltersChange={onFiltersChange}
|
onFiltersChange={onFiltersChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<ToggleButton
|
||||||
onClick={onToggleCompactView}
|
variant="secondary"
|
||||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
|
isActive={compactView}
|
||||||
compactView
|
onClick={onToggleCompactView}
|
||||||
? 'bg-[var(--secondary)]/20 text-[var(--secondary)] border border-[var(--secondary)]/30'
|
title={compactView ? "Vue détaillée" : "Vue compacte"}
|
||||||
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--secondary)]/50'
|
icon={
|
||||||
}`}
|
|
||||||
title={compactView ? "Vue détaillée" : "Vue compacte"}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
{compactView ? (
|
{compactView ? (
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||||
)}
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
{compactView ? 'Détaillée' : 'Compacte'}
|
}
|
||||||
</button>
|
>
|
||||||
|
{compactView ? 'Détaillée' : 'Compacte'}
|
||||||
|
</ToggleButton>
|
||||||
|
|
||||||
<button
|
<ToggleButton
|
||||||
onClick={onToggleSwimlanes}
|
variant="warning"
|
||||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
|
isActive={swimlanesByTags}
|
||||||
swimlanesByTags
|
onClick={onToggleSwimlanes}
|
||||||
? 'bg-[var(--warning)]/20 text-[var(--warning)] border border-[var(--warning)]/30'
|
title={swimlanesByTags ? "Vue standard" : "Vue swimlanes"}
|
||||||
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--warning)]/50'
|
icon={
|
||||||
}`}
|
|
||||||
title={swimlanesByTags ? "Vue standard" : "Vue swimlanes"}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
{swimlanesByTags ? (
|
{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" />
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14-7H5m14 14H5" />
|
||||||
)}
|
)}
|
||||||
</svg>
|
</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">
|
{swimlanesByTags ? 'Standard' : 'Swimlanes'}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
</ToggleButton>
|
||||||
</svg>
|
|
||||||
Nouvelle tâche
|
{/* Font Size Toggle */}
|
||||||
</Button>
|
<FontSizeToggle />
|
||||||
</div>
|
</ControlGroup>
|
||||||
</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>
|
||||||
|
</ControlSection>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ControlPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { TaskPriority, TaskStatus } from '@/lib/types';
|
import { TaskPriority, TaskStatus } from '@/lib/types';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button, SearchInput, ToggleButton, ControlPanel, ControlSection, ControlGroup, FilterSummary } from '@/components/ui';
|
||||||
import { Input } from '@/components/ui/Input';
|
|
||||||
import { useTasksContext } from '@/contexts/TasksContext';
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
import { SORT_OPTIONS } from '@/lib/sort-config';
|
import { SORT_OPTIONS } from '@/lib/sort-config';
|
||||||
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
|
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
|
||||||
@@ -57,47 +56,11 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
|
|||||||
}
|
}
|
||||||
}, [isSortExpanded, isSwimlaneModeExpanded]);
|
}, [isSortExpanded, isSwimlaneModeExpanded]);
|
||||||
|
|
||||||
// État local pour la recherche pour une saisie fluide
|
// Handler pour la recherche avec debounce intégré
|
||||||
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]);
|
|
||||||
|
|
||||||
const handleSearchChange = (search: string) => {
|
const handleSearchChange = (search: string) => {
|
||||||
isUserTypingRef.current = true;
|
onFiltersChange({ ...filters, search: search || undefined });
|
||||||
setLocalSearch(search);
|
|
||||||
debouncedSearchChange(search);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 handleTagToggle = (tagName: string) => {
|
||||||
const currentTags = filters.tags || [];
|
const currentTags = filters.tags || [];
|
||||||
const newTags = currentTags.includes(tagName)
|
const newTags = currentTags.includes(tagName)
|
||||||
@@ -180,48 +143,48 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<div className="container mx-auto px-6 py-4">
|
||||||
{/* Header avec recherche et bouton expand */}
|
{/* Header avec recherche et bouton expand */}
|
||||||
<div className="flex items-center gap-4">
|
<ControlSection>
|
||||||
<div className="flex-1 max-w-md">
|
<div className="flex-1 max-w-md">
|
||||||
<Input
|
<SearchInput
|
||||||
type="text"
|
value={filters.search || ''}
|
||||||
value={localSearch}
|
onChange={handleSearchChange}
|
||||||
onChange={(e) => handleSearchChange(e.target.value)}
|
|
||||||
placeholder="Rechercher des tâches..."
|
placeholder="Rechercher des tâches..."
|
||||||
className="bg-[var(--card)] border-[var(--border)]"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Menu swimlanes - masqué sur mobile */}
|
{/* Menu swimlanes - masqué sur mobile */}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div className="flex gap-1">
|
<ControlGroup>
|
||||||
<Button
|
<ToggleButton
|
||||||
variant={filters.swimlanesByTags ? "primary" : "ghost"}
|
variant="warning"
|
||||||
|
isActive={!!filters.swimlanesByTags}
|
||||||
onClick={handleSwimlanesToggle}
|
onClick={handleSwimlanesToggle}
|
||||||
className="flex items-center gap-2"
|
|
||||||
title="Mode d'affichage"
|
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
|
{!filters.swimlanesByTags
|
||||||
? 'Normal'
|
? 'Normal'
|
||||||
: filters.swimlanesMode === 'priority'
|
: filters.swimlanesMode === 'priority'
|
||||||
? 'Par priorité'
|
? 'Par priorité'
|
||||||
: 'Par tags'
|
: 'Par tags'
|
||||||
}
|
}
|
||||||
</Button>
|
</ToggleButton>
|
||||||
|
|
||||||
{/* Bouton pour changer le mode des swimlanes */}
|
{/* Bouton pour changer le mode des swimlanes */}
|
||||||
{filters.swimlanesByTags && (
|
{filters.swimlanesByTags && (
|
||||||
@@ -240,7 +203,7 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
|
|||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ControlGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
@@ -273,16 +236,7 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeFiltersCount > 0 && (
|
</ControlSection>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={handleClearFilters}
|
|
||||||
className="text-[var(--muted-foreground)] hover:text-[var(--destructive)]"
|
|
||||||
>
|
|
||||||
Effacer
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filtres étendus */}
|
{/* Filtres étendus */}
|
||||||
<div className="mt-4 border-t border-[var(--border)]/50 pt-4">
|
<div className="mt-4 border-t border-[var(--border)]/50 pt-4">
|
||||||
@@ -331,70 +285,12 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Résumé des filtres actifs */}
|
{/* Résumé des filtres actifs */}
|
||||||
{activeFiltersCount > 0 && (
|
<FilterSummary
|
||||||
<div className="bg-[var(--card)]/30 rounded-lg p-3 border border-[var(--border)]/50 mt-4">
|
filters={filters}
|
||||||
<div className="text-xs text-[var(--muted-foreground)] font-mono uppercase tracking-wider mb-2">
|
activeFiltersCount={activeFiltersCount}
|
||||||
Filtres actifs
|
onClearFilters={handleClearFilters}
|
||||||
</div>
|
className="mt-6"
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -464,6 +360,6 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
|
|||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
</div>
|
</ControlPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button, ToggleButton, ControlPanel } from '@/components/ui';
|
||||||
import { SourceQuickFilter } from '@/components/kanban/SourceQuickFilter';
|
import { SourceQuickFilter } from '@/components/kanban/SourceQuickFilter';
|
||||||
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
|
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
|
||||||
import type { KanbanFilters } from '@/lib/types';
|
import type { KanbanFilters } from '@/lib/types';
|
||||||
@@ -34,102 +34,96 @@ export function MobileControls({
|
|||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[var(--card)]/30 border-b border-[var(--border)]/30">
|
<ControlPanel className="px-4 py-2">
|
||||||
<div className="px-4 py-2">
|
{/* Barre principale mobile */}
|
||||||
{/* Barre principale mobile */}
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
{/* Bouton menu hamburger */}
|
||||||
{/* Bouton menu hamburger */}
|
<ToggleButton
|
||||||
<button
|
variant="primary"
|
||||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
isActive={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"
|
count={activeFiltersCount}
|
||||||
>
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||||
|
icon={
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||||
</svg>
|
</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">
|
Options
|
||||||
{activeFiltersCount}
|
</ToggleButton>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Bouton d'ajout de tâche */}
|
{/* Bouton d'ajout de tâche */}
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={onCreateTask}
|
onClick={onCreateTask}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="hidden xs:inline">Nouvelle</span>
|
<span className="hidden xs:inline">Nouvelle</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Menu déroulant */}
|
{/* Menu déroulant */}
|
||||||
{isMenuOpen && (
|
{isMenuOpen && (
|
||||||
<div className="mt-3 p-3 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg">
|
<div className="mt-3 p-3 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg">
|
||||||
{/* Section Affichage */}
|
{/* Section Affichage */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
|
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
|
||||||
Affichage
|
Affichage
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<button
|
<ToggleButton
|
||||||
onClick={() => {
|
variant="primary"
|
||||||
onToggleFilters();
|
isActive={showFilters}
|
||||||
setIsMenuOpen(false);
|
onClick={() => {
|
||||||
}}
|
onToggleFilters();
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono transition-all ${
|
setIsMenuOpen(false);
|
||||||
showFilters
|
}}
|
||||||
? 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30'
|
icon={
|
||||||
: 'bg-[var(--muted)]/30 text-[var(--muted-foreground)] border border-[var(--border)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
Filtres
|
}
|
||||||
</button>
|
>
|
||||||
|
Filtres
|
||||||
|
</ToggleButton>
|
||||||
|
|
||||||
<button
|
<ToggleButton
|
||||||
onClick={() => {
|
variant="accent"
|
||||||
onToggleObjectives();
|
isActive={showObjectives}
|
||||||
setIsMenuOpen(false);
|
onClick={() => {
|
||||||
}}
|
onToggleObjectives();
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono transition-all ${
|
setIsMenuOpen(false);
|
||||||
showObjectives
|
}}
|
||||||
? 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30'
|
icon={
|
||||||
: 'bg-[var(--muted)]/30 text-[var(--muted-foreground)] border border-[var(--border)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
Objectifs
|
}
|
||||||
</button>
|
>
|
||||||
</div>
|
Objectifs
|
||||||
|
</ToggleButton>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Section Paramètres */}
|
{/* Section Paramètres */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
|
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
|
||||||
Paramètres
|
Paramètres
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<button
|
<ToggleButton
|
||||||
onClick={() => {
|
variant="secondary"
|
||||||
onToggleCompactView();
|
isActive={compactView}
|
||||||
setIsMenuOpen(false);
|
onClick={() => {
|
||||||
}}
|
onToggleCompactView();
|
||||||
className={`w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono transition-all ${
|
setIsMenuOpen(false);
|
||||||
compactView
|
}}
|
||||||
? 'bg-[var(--secondary)]/20 text-[var(--secondary)] border border-[var(--secondary)]/30'
|
className="w-full"
|
||||||
: 'bg-[var(--muted)]/30 text-[var(--muted-foreground)] border border-[var(--border)]'
|
icon={
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
{compactView ? (
|
{compactView ? (
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||||
)}
|
)}
|
||||||
</svg>
|
</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">
|
<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>
|
<span className="text-sm font-mono text-[var(--muted-foreground)]">Taille police</span>
|
||||||
<FontSizeToggle />
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
{/* Section Sources */}
|
||||||
</div>
|
<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="bg-[var(--card)]/30 border-b border-[var(--accent)]/30">
|
||||||
<div className="container mx-auto px-6 py-4">
|
<div className="container mx-auto px-6 py-4">
|
||||||
<Card variant="column" className="border-[var(--accent)]/30 shadow-[var(--accent)]/10">
|
<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">
|
<div className="flex items-center justify-between">
|
||||||
<button
|
<button
|
||||||
onClick={toggleObjectivesCollapse}
|
onClick={toggleObjectivesCollapse}
|
||||||
@@ -172,7 +172,7 @@ export function ObjectivesBoard({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-3">
|
||||||
{(() => {
|
{(() => {
|
||||||
// Séparer les tâches par statut
|
// Séparer les tâches par statut
|
||||||
const inProgressTasks = tasks.filter(task => task.status === 'in_progress');
|
const inProgressTasks = tasks.filter(task => task.status === 'in_progress');
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { TaskStatus, Task } from '@/lib/types';
|
import { TaskStatus, Task } from '@/lib/types';
|
||||||
import { getAllStatuses } from '@/lib/status-config';
|
import { getAllStatuses } from '@/lib/status-config';
|
||||||
|
import { FilterChip } from '@/components/ui';
|
||||||
|
|
||||||
interface ColumnFiltersProps {
|
interface ColumnFiltersProps {
|
||||||
hiddenStatuses: Set<TaskStatus>;
|
hiddenStatuses: Set<TaskStatus>;
|
||||||
@@ -19,18 +20,18 @@ export function ColumnFilters({ hiddenStatuses, onToggleStatus, tasks }: ColumnF
|
|||||||
{getAllStatuses().map(statusConfig => {
|
{getAllStatuses().map(statusConfig => {
|
||||||
const statusCount = tasks.filter(task => task.status === statusConfig.key).length;
|
const statusCount = tasks.filter(task => task.status === statusConfig.key).length;
|
||||||
return (
|
return (
|
||||||
<button
|
<FilterChip
|
||||||
key={statusConfig.key}
|
key={statusConfig.key}
|
||||||
onClick={() => onToggleStatus(statusConfig.key)}
|
onClick={() => onToggleStatus(statusConfig.key)}
|
||||||
className={`px-2 py-1 rounded border text-xs font-medium transition-colors ${
|
variant={hiddenStatuses.has(statusConfig.key) ? 'hidden' : 'default'}
|
||||||
hiddenStatuses.has(statusConfig.key)
|
count={statusCount}
|
||||||
? 'bg-[var(--muted)]/20 text-[var(--muted)] border-[var(--muted)]/30 hover:bg-[var(--muted)]/30'
|
icon={
|
||||||
: 'bg-[var(--primary)]/20 text-[var(--primary)] border-[var(--primary)]/30 hover:bg-[var(--primary)]/30'
|
hiddenStatuses.has(statusConfig.key) ? '👁️🗨️' : '👁️'
|
||||||
}`}
|
}
|
||||||
title={hiddenStatuses.has(statusConfig.key) ? `Afficher ${statusConfig.label}` : `Masquer ${statusConfig.label}`}
|
title={hiddenStatuses.has(statusConfig.key) ? `Afficher ${statusConfig.label}` : `Masquer ${statusConfig.label}`}
|
||||||
>
|
>
|
||||||
{hiddenStatuses.has(statusConfig.key) ? '👁️🗨️' : '👁️'} {statusConfig.label}{statusCount ? ` (${statusCount})` : ''}
|
{statusConfig.label}
|
||||||
</button>
|
</FilterChip>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { FilterChip } from '@/components/ui';
|
||||||
|
|
||||||
interface GeneralFiltersProps {
|
interface GeneralFiltersProps {
|
||||||
showWithDueDate?: boolean;
|
showWithDueDate?: boolean;
|
||||||
onDueDateFilterToggle: () => void;
|
onDueDateFilterToggle: () => void;
|
||||||
@@ -12,25 +14,22 @@ export function GeneralFilters({ showWithDueDate = false, onDueDateFilterToggle
|
|||||||
Généraux
|
Généraux
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
<button
|
<FilterChip
|
||||||
type="button"
|
|
||||||
onClick={onDueDateFilterToggle}
|
onClick={onDueDateFilterToggle}
|
||||||
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium cursor-pointer ${
|
variant={showWithDueDate ? 'selected' : 'default'}
|
||||||
showWithDueDate
|
icon={
|
||||||
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
|
<svg
|
||||||
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)] hover:bg-[var(--card)]/80'
|
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
|
Avec date de fin
|
||||||
</button>
|
</FilterChip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button, FilterChip } from '@/components/ui';
|
||||||
import { useTasksContext } from '@/contexts/TasksContext';
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
import type { KanbanFilters } from '@/lib/types';
|
import type { KanbanFilters } from '@/lib/types';
|
||||||
|
|
||||||
@@ -160,17 +160,15 @@ export function JiraFilters({ filters, onFiltersChange }: JiraFiltersProps) {
|
|||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{availableJiraProjects.map((project) => (
|
{availableJiraProjects.map((project) => (
|
||||||
<button
|
<FilterChip
|
||||||
key={project}
|
key={project}
|
||||||
onClick={() => handleJiraProjectToggle(project)}
|
onClick={() => handleJiraProjectToggle(project)}
|
||||||
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
|
variant={filters.jiraProjects?.includes(project) ? 'selected' : 'default'}
|
||||||
filters.jiraProjects?.includes(project)
|
count={jiraProjectCounts[project]}
|
||||||
? 'border-blue-400 bg-blue-400/10 text-blue-400'
|
icon="📋"
|
||||||
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
📋 {project} ({jiraProjectCounts[project]})
|
{project}
|
||||||
</button>
|
</FilterChip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,25 +181,31 @@ export function JiraFilters({ filters, onFiltersChange }: JiraFiltersProps) {
|
|||||||
Types
|
Types
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{availableJiraTypes.map((type) => (
|
{availableJiraTypes.map((type) => {
|
||||||
<button
|
const getTypeIcon = (type: string) => {
|
||||||
key={type}
|
switch (type) {
|
||||||
onClick={() => handleJiraTypeToggle(type)}
|
case 'Feature': return '✨';
|
||||||
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
|
case 'Story': return '📖';
|
||||||
filters.jiraTypes?.includes(type)
|
case 'Task': return '📝';
|
||||||
? 'border-purple-400 bg-purple-400/10 text-purple-400'
|
case 'Bug': return '🐛';
|
||||||
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
|
case 'Support': return '🛠️';
|
||||||
}`}
|
case 'Enabler': return '🔧';
|
||||||
>
|
default: return '📋';
|
||||||
{type === 'Feature' && '✨ '}
|
}
|
||||||
{type === 'Story' && '📖 '}
|
};
|
||||||
{type === 'Task' && '📝 '}
|
|
||||||
{type === 'Bug' && '🐛 '}
|
return (
|
||||||
{type === 'Support' && '🛠️ '}
|
<FilterChip
|
||||||
{type === 'Enabler' && '🔧 '}
|
key={type}
|
||||||
{type} ({jiraTypeCounts[type]})
|
onClick={() => handleJiraTypeToggle(type)}
|
||||||
</button>
|
variant={filters.jiraTypes?.includes(type) ? 'selected' : 'default'}
|
||||||
))}
|
count={jiraTypeCounts[type]}
|
||||||
|
icon={getTypeIcon(type)}
|
||||||
|
>
|
||||||
|
{type}
|
||||||
|
</FilterChip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useMemo } from 'react';
|
|||||||
import { TaskPriority } from '@/lib/types';
|
import { TaskPriority } from '@/lib/types';
|
||||||
import { useTasksContext } from '@/contexts/TasksContext';
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
import { getAllPriorities, getPriorityColorHex } from '@/lib/status-config';
|
import { getAllPriorities, getPriorityColorHex } from '@/lib/status-config';
|
||||||
|
import { FilterChip } from '@/components/ui';
|
||||||
|
|
||||||
interface PriorityFiltersProps {
|
interface PriorityFiltersProps {
|
||||||
selectedPriorities?: TaskPriority[];
|
selectedPriorities?: TaskPriority[];
|
||||||
@@ -44,22 +45,16 @@ export function PriorityFilters({ selectedPriorities = [], onPriorityToggle }: P
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{visiblePriorities.map((priority) => (
|
{visiblePriorities.map((priority) => (
|
||||||
<button
|
<FilterChip
|
||||||
key={priority.value}
|
key={priority.value}
|
||||||
onClick={() => onPriorityToggle(priority.value)}
|
onClick={() => onPriorityToggle(priority.value)}
|
||||||
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium ${
|
variant={selectedPriorities.includes(priority.value) ? 'selected' : 'priority'}
|
||||||
selectedPriorities.includes(priority.value)
|
color={getPriorityColorHex(priority.color)}
|
||||||
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
|
count={priority.count}
|
||||||
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
|
>
|
||||||
}`}
|
{priority.label}
|
||||||
>
|
</FilterChip>
|
||||||
<div
|
))}
|
||||||
className="w-2 h-2 rounded-full"
|
|
||||||
style={{ backgroundColor: getPriorityColorHex(priority.color) }}
|
|
||||||
/>
|
|
||||||
{priority.label} ({priority.count})
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTasksContext } from '@/contexts/TasksContext';
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
|
import { FilterChip } from '@/components/ui';
|
||||||
|
|
||||||
interface TagFiltersProps {
|
interface TagFiltersProps {
|
||||||
selectedTags?: string[];
|
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">
|
<div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto">
|
||||||
{visibleTags.map((tag) => (
|
{visibleTags.map((tag) => (
|
||||||
<button
|
<FilterChip
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
onClick={() => onTagToggle(tag.name)}
|
onClick={() => onTagToggle(tag.name)}
|
||||||
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium ${
|
variant={selectedTags.includes(tag.name) ? 'selected' : 'tag'}
|
||||||
selectedTags.includes(tag.name)
|
color={tag.color}
|
||||||
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
|
count={tagCounts[tag.name]}
|
||||||
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
|
>
|
||||||
}`}
|
{tag.name}
|
||||||
>
|
</FilterChip>
|
||||||
<div
|
))}
|
||||||
className="w-2 h-2 rounded-full"
|
|
||||||
style={{ backgroundColor: tag.color }}
|
|
||||||
/>
|
|
||||||
{tag.name} ({tagCounts[tag.name]})
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button, FilterChip } from '@/components/ui';
|
||||||
import { useTasksContext } from '@/contexts/TasksContext';
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
import type { KanbanFilters } from '@/lib/types';
|
import type { KanbanFilters } from '@/lib/types';
|
||||||
|
|
||||||
@@ -125,17 +125,15 @@ export function TfsFilters({ filters, onFiltersChange }: TfsFiltersProps) {
|
|||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{availableTfsProjects.map((project) => (
|
{availableTfsProjects.map((project) => (
|
||||||
<button
|
<FilterChip
|
||||||
key={project}
|
key={project}
|
||||||
onClick={() => handleTfsProjectToggle(project)}
|
onClick={() => handleTfsProjectToggle(project)}
|
||||||
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
|
variant={filters.tfsProjects?.includes(project) ? 'selected' : 'default'}
|
||||||
filters.tfsProjects?.includes(project)
|
count={tfsProjectCounts[project]}
|
||||||
? 'border-orange-400 bg-orange-400/10 text-orange-400'
|
icon="📦"
|
||||||
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
📦 {project} ({tfsProjectCounts[project]})
|
{project}
|
||||||
</button>
|
</FilterChip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Alert, AlertTitle, AlertDescription } from '@/components/ui/Alert';
|
|||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { StyledCard } from '@/components/ui/StyledCard';
|
import { StyledCard } from '@/components/ui/StyledCard';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
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';
|
import { ThemeSelector } from '@/components/ThemeSelector';
|
||||||
|
|
||||||
export function UIShowcaseClient() {
|
export function UIShowcaseClient() {
|
||||||
@@ -562,6 +562,243 @@ export function UIShowcaseClient() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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 */}
|
{/* Footer */}
|
||||||
<div className="text-center pt-8 border-t border-[var(--border)]">
|
<div className="text-center pt-8 border-t border-[var(--border)]">
|
||||||
<p className="text-[var(--muted-foreground)]">
|
<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 { TaskCard } from './TaskCard';
|
||||||
export { MetricCard } from './MetricCard';
|
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
|
// Composants existants
|
||||||
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
|
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
|
||||||
export { Header } from './Header';
|
export { Header } from './Header';
|
||||||
|
|||||||
Reference in New Issue
Block a user