feat: update TODO.md and enhance dashboard components

- Marked several UI/UX tasks as complete in TODO.md, including improvements for Kanban icons, tag visibility, recent tasks display, and header responsiveness.
- Updated PriorityDistributionChart to adjust height for better layout.
- Refined IntegrationFilter to improve filter display and added new trigger class for dropdowns.
- Replaced RecentTaskTimeline with TaskCard in RecentTasks for better consistency.
- Enhanced TagDistributionChart with improved tooltip and legend styling.
- Updated DesktopControls and MobileControls to use lucide-react icons for filters and search functionality.
- Removed RecentTaskTimeline component for cleaner codebase.
This commit is contained in:
Julien Froidefond
2025-10-04 07:17:39 +02:00
parent c7ad1c0416
commit eac9e9a0bb
14 changed files with 213 additions and 302 deletions

16
TODO.md
View File

@@ -10,12 +10,12 @@
### 🎨 Design et Interface
- [X] **Homepage cards** : toute en variant glass
- [ ] **Icône Kanban homepage** - Changer icône sur la page d'accueil, pas lisible (utiliser une lib)
- [ ] **Lisibilité label graph par tag** - Améliorer la lisibilité des labels dans les graphiques par tag
- [ ] **Tag homepage** - Problème d'affichage des graphs de tags sur la homepage coté lisibilité, cezrtaines icones ne sont pas entièrement visible, et la légende est trop proche du graphe.
- [ ] **Tâches récentes** - Revoir l'affichage et la logique des tâches récentes
- [ ] **Header dépasse en tablet** - Corriger le débordement du header sur tablette
- [ ] **Icônes agenda et filtres** - Améliorer les icônes de l'agenda et des filtres dans desktop controls (utiliser une lib)
- [X] **Icône Kanban homepage** - Changer icône sur la page d'accueil, pas lisible (utiliser une lib)
- [x] **Lisibilité label graph par tag** - Améliorer la lisibilité des labels dans les graphiques par tag <!-- Amélioré marges, légendes, tailles de police, retiré emojis -->
- [x] **Tag homepage** - Problème d'affichage des graphs de tags sur la homepage côté lisibilité, certaines icones ne sont pas entièrement visible, et la légende est trop proche du graphe. <!-- Amélioré hauteur, marges, responsive -->
- [x] **Tâches récentes** - Revoir l'affichage et la logique des tâches récentes <!-- Logique améliorée (tâches terminées récentes), responsive, icône claire -->
- [x] **Header dépasse en tablet** - Corriger le débordement du header sur tablette <!-- Responsive amélioré, taille réglée, navigation adaptative -->
- [x] **Icônes agenda et filtres** - Améliorer les icônes de l'agenda et des filtres dans desktop controls (utiliser une lib) <!-- Clock pour échéance, Settings pour filtres, Search visuelle -->
- [ ] **Réunion/tâche design** - Revoir le design des bouton dans dailySectrion : les toggles avoir un compposant ui
- [ ] **Légende calendrier et padding** - Corriger l'espacement et la légende du calendrier dans daily
- [ ] **EditModal task couleur calendrier** - Problème de couleur en ajout de taches dans tous les icones calendriers; colmler au thème
@@ -29,13 +29,13 @@
- [ ] **Deux modales** - Problème de duplication de modales
- [ ] **Control panel et select** - Problème avec les contrôles et sélecteurs
- [ ] **TaskCard et Kanban transparence** - Appliquer la transparence sur le background et non sur la card
- [ ] **Recherche Kanban desktop controls** - Ajouter icône et label : "rechercher" pour rapetir
- [X] **Recherche Kanban desktop controls** - Ajouter icône et label : "rechercher" pour rapetir
- [ ] **Largeur page Kanban** - Réduire légèrement la largeur et revoir toutes les autres pages
- [ ] **Icône thème à gauche du profil** - Repositionner l'icône de thème dans le header
- [ ] **Déconnexion trop petit et couleur** - Améliorer le bouton de déconnexion
- [ ] **Fond modal trop opaque** - Réduire l'opacité du fond des modales
- [ ] **Couleurs thème clair et TFS Jira Kanban** - Harmoniser les couleurs du thème clair
- [ ] **États sélectionnés desktop control** - Revoir les couleurs des états sélectionnés pour avoir le joli bleu du dropdown partout
- [X] **États sélectionnés desktop control** - Revoir les couleurs des états sélectionnés pour avoir le joli bleu du dropdown partout
- [ ] **Dépasse 1000 caractères en edit modal task** - Corriger la limite (pas de limite) et revoir la quickcard description
- [ ] **UI si échéance et trop de labels dans le footer de card** - Améliorer l'affichage en mode détaillé TaskCard; certains boutons sont sur deux lignes ce qui casse l'affichage
- [ ] **Gravatar** - Implémenter l'affichage des avatars Gravatar

View File

@@ -63,7 +63,7 @@ export function PriorityDistributionChart({ data, title = "Distribution des Prio
return (
<Card variant="glass" className="p-6">
<h3 className="text-lg font-semibold mb-4">{title}</h3>
<div className="h-64">
<div className="h-70">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie

View File

@@ -168,9 +168,9 @@ export function IntegrationFilter({
} else if (activeFilters.length === 1) {
const source = activeFilters[0];
const mode = getSourceMode(source.id);
return mode === 'show' ? `Seulement ${source.label}` : `Sans ${source.label}`;
return mode === 'show' ? `Solo ${source.label}` : `-${source.label}`;
} else {
return `${activeFilters.length} filtres actifs`;
return `+${activeFilters.length}`;
}
};
@@ -268,7 +268,8 @@ export function IntegrationFilter({
variant={getMainButtonVariant()}
content={dropdownContent}
placement={alignRight ? "bottom-end" : "bottom-start"}
className={`min-w-[240px] ${alignRight ? "transform -translate-x-full" : ""}`}
className={`min-w-[239px] max-h-[190px] overflow-y-auto ${alignRight ? "transform -translate-x-full" : ""}`}
triggerClassName="h-[33px] py-1 max-w-[140px] truncate"
/>
);
}

View File

@@ -2,10 +2,10 @@
import { Task } from '@/lib/types';
import { Card } from '@/components/ui/Card';
import { RecentTaskTimeline } from '@/components/ui/RecentTaskTimeline';
import { TaskCard } from '@/components/ui/TaskCard';
import { useTasksContext } from '@/contexts/TasksContext';
import Link from 'next/link';
import { Clipboard } from 'lucide-react';
import { Clipboard, Clock } from 'lucide-react';
interface RecentTasksProps {
tasks: Task[];
@@ -31,34 +31,54 @@ export function RecentTasks({ tasks, selectedSources = [], hiddenSources = [] }:
);
}
// Prendre les 5 tâches les plus récentes (créées ou modifiées)
// Prendre les 5 tâches les plus pertinentes (créées récemment ou modifiées récemment)
const recentTasks = filteredTasks
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
.filter(task => {
// Ne pas afficher les tâches terminées depuis plus de 7 jours
if (task.status === 'done' && task.completedAt) {
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
return task.completedAt > sevenDaysAgo;
}
return task.status !== 'done';
})
.sort((a, b) => {
// Prioriser les tâches non terminées et récentes
if (a.status === 'done' && b.status !== 'done') return 1;
if (b.status === 'done' && a.status !== 'done') return -1;
// Sinon trier par date de modification
return b.updatedAt.getTime() - a.updatedAt.getTime();
})
.slice(0, 5);
return (
<Card variant="glass" className="p-6 mt-8">
<Card variant="glass" className="p-4 sm:p-6 mt-8">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Tâches Récentes</h3>
<div className="flex items-center gap-3">
<Clock className="w-5 h-5 text-[var(--primary)]" />
<h3 className="text-lg font-semibold text-[var(--foreground)]">Tâches Récentes</h3>
</div>
<Link href="/kanban">
<button className="text-sm text-[var(--primary)] hover:underline">
Voir toutes
<button className="text-sm text-[var(--primary)] hover:text-[var(--primary)]/80 hover:underline font-medium transition-colors">
Voir toutes
</button>
</Link>
</div>
{recentTasks.length === 0 ? (
<div className="text-center py-8 text-[var(--muted-foreground)]">
<Clipboard className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p>Aucune tâche disponible</p>
<p className="text-sm">Créez votre première tâche pour commencer</p>
<div className="text-center py-6 sm:py-8 text-[var(--muted-foreground)]">
<Clipboard className="w-8 h-8 sm:w-12 sm:h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm sm:text-base">Aucune tâche récente</p>
<p className="text-xs sm:text-sm opacity-75">Créez une nouvelle tâche pour commencer</p>
</div>
) : (
<div className="space-y-1">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{recentTasks.map((task) => (
<RecentTaskTimeline
<TaskCard
key={task.id}
variant="detailed"
title={task.title}
description={task.description}
status={task.status}
@@ -66,13 +86,10 @@ export function RecentTasks({ tasks, selectedSources = [], hiddenSources = [] }:
tags={task.tags || []}
dueDate={task.dueDate}
completedAt={task.completedAt}
updatedAt={task.updatedAt}
source={task.source || 'manual'}
jiraKey={task.jiraKey}
tfsPullRequestId={task.tfsPullRequestId}
availableTags={availableTags}
onClick={() => {
// Navigation vers le kanban avec la tâche sélectionnée
window.location.href = `/kanban?taskId=${task.id}`;
}}
/>

View File

@@ -10,31 +10,33 @@ interface TagDistributionChartProps {
}
export function TagDistributionChart({ metrics, className }: TagDistributionChartProps) {
// Préparer les données pour le graphique en camembert
const pieData = metrics.tagDistribution.slice(0, 8).map((tag) => ({
name: tag.tagName,
value: tag.count,
percentage: tag.percentage,
color: tag.tagColor
color: tag.tagColor // Garder la couleur originale du tag
}));
// Préparer les données pour le graphique en barres (top tags)
const barData = metrics.topTags.slice(0, 10).map(tag => ({
const barData = metrics.topTags.slice(0, 10).map((tag) => ({
name: tag.tagName.length > 12 ? `${tag.tagName.substring(0, 12)}...` : tag.tagName,
usage: tag.usage,
completionRate: tag.completionRate,
avgPriority: tag.avgPriority,
color: tag.tagColor
color: tag.tagColor // Garder la couleur originale du tag
}));
// Tooltip personnalisé pour le camembert
const PieTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: { name: string; value: number; percentage: number } }> }) => {
// Tooltip personnalisé pour les tags (distribution)
const TagTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: { name: string; value: number; percentage: number } }> }) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg">
<p className="font-medium mb-1">{data.name}</p>
<p className="text-sm text-[var(--foreground)]">
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-4 shadow-xl">
<p className="font-semibold mb-2 text-[var(--foreground)]">{data.name}</p>
<p className="text-sm text-[var(--muted-foreground)]">
{data.value} tâches ({data.percentage.toFixed(1)}%)
</p>
</div>
@@ -48,12 +50,12 @@ export function TagDistributionChart({ metrics, className }: TagDistributionChar
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg">
<p className="font-medium mb-1">{data.name}</p>
<p className="text-sm text-[var(--foreground)]">
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-4 shadow-xl">
<p className="font-semibold mb-2 text-[var(--foreground)]">{data.name}</p>
<p className="text-sm text-[var(--muted-foreground)] mb-1">
{data.usage} tâches
</p>
<p className="text-sm text-[var(--muted-foreground)]">
<p className="text-xs text-[var(--muted-foreground)]">
Taux de completion: {data.completionRate.toFixed(1)}%
</p>
</div>
@@ -62,19 +64,17 @@ export function TagDistributionChart({ metrics, className }: TagDistributionChar
return null;
};
// Légende personnalisée
const CustomLegend = ({ payload }: { payload?: Array<{ value: string; color: string }> }) => {
if (!payload) return null;
// Légende personnalisée qui utilise directement les données du graphique
const CustomLegend = () => {
return (
<div className="flex flex-wrap gap-2 mt-4">
{payload.map((entry, index) => (
<div key={index} className="flex items-center gap-2 text-sm">
<div className="flex flex-wrap gap-4 mt-6">
{pieData.map((entry, index) => (
<div key={index} className="flex items-center gap-3">
<div
className="w-3 h-3 rounded-full"
className="w-4 h-4 rounded-full border border-[var(--border)]"
style={{ backgroundColor: entry.color }}
/>
<span className="text-[var(--foreground)]">{entry.value}</span>
<span className="text-sm text-[var(--muted-foreground)]">{entry.name}</span>
</div>
))}
</div>
@@ -83,56 +83,60 @@ export function TagDistributionChart({ metrics, className }: TagDistributionChar
return (
<div className={className}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
{/* Distribution par tags - Camembert */}
<Card variant="glass" className="p-6">
<h3 className="text-lg font-semibold mb-4">🏷 Distribution par Tags</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={pieData}
cx="50%"
cy="50%"
labelLine={false}
label={(props: PieLabelRenderProps) => {
const { name, percent } = props;
const percentValue = typeof percent === 'number' ? percent : 0;
return percentValue > 0.05 ? `${name}: ${(percentValue * 100).toFixed(1)}%` : '';
}}
outerRadius={80}
fill="#8884d8"
dataKey="value"
nameKey="name"
>
{pieData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip content={<PieTooltip />} />
<Legend content={<CustomLegend />} />
</PieChart>
</ResponsiveContainer>
</div>
</Card>
<Card variant="glass" className="p-6">
<h3 className="text-lg font-semibold mb-4 text-[var(--foreground)]">Distribution par Tags</h3>
<div className="h-60">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={pieData}
cx="50%"
cy="50%"
labelLine={false}
label={(props: PieLabelRenderProps) => {
const { percent } = props;
return typeof percent === 'number' && percent > 0.05 ? `${Math.round(percent * 100)}%` : '';
}}
outerRadius={80}
fill="#8884d8"
dataKey="value"
nameKey="name"
>
{pieData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip content={<TagTooltip />} />
</PieChart>
</ResponsiveContainer>
<CustomLegend />
</div>
</Card>
{/* Top tags - Barres */}
<Card variant="glass" className="p-6">
<h3 className="text-lg font-semibold mb-4">📊 Top Tags par Usage</h3>
<h3 className="text-lg font-semibold mb-4 text-[var(--foreground)]">Top Tags par Usage</h3>
<div className="h-64">
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={barData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<BarChart data={barData} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis
dataKey="name"
tick={{ fontSize: 12 }}
tick={{ fontSize: 13, fill: 'var(--muted-foreground)' }}
angle={-45}
textAnchor="end"
height={80}
style={{ fontFamily: 'ui-monospace, SFMono-Regular, monospace' }}
/>
<YAxis
tick={{ fontSize: 13, fill: 'var(--foreground)' }}
style={{ fontFamily: 'ui-monospace, SFMono-Regular, monospace' }}
/>
<YAxis tick={{ fontSize: 12 }} />
<Tooltip content={<BarTooltip />} />
<Bar
dataKey="usage"
@@ -149,8 +153,8 @@ export function TagDistributionChart({ metrics, className }: TagDistributionChar
</div>
{/* Statistiques des tags */}
<Card variant="glass" className="p-6 mt-6">
<h3 className="text-lg font-semibold mb-4">📈 Statistiques des Tags</h3>
<Card variant="glass" className="p-6 mt-8">
<h3 className="text-lg font-semibold mb-4 text-[var(--foreground)]">Statistiques des Tags</h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div className="text-center">

View File

@@ -5,7 +5,7 @@ import { Button, ToggleButton, SearchInput, ControlPanel, ControlSection, Contro
import { IntegrationFilter } from '@/components/dashboard/IntegrationFilter';
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
import type { KanbanFilters } from '@/lib/types';
import { Filter, Target, Calendar, Plus, List, Grid3X3, Layout } from 'lucide-react';
import { Target, Plus, List, Grid3X3, Layout, Clock, Search, Settings } from 'lucide-react';
interface DesktopControlsProps {
showFilters: boolean;
@@ -81,13 +81,17 @@ export function DesktopControls({
{/* 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..."
/>
<ControlSection className="items-center gap-2">
{/* Champ de recherche avec icône */}
<div className="relative flex-1 min-w-0">
<SearchInput
value={localSearch}
onChange={handleSearchChange}
placeholder="Rechercher"
className="pl-10"
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--muted-foreground)] pointer-events-none" />
</div>
<ControlGroup>
<ToggleButton
@@ -95,7 +99,7 @@ export function DesktopControls({
isActive={showFilters}
count={activeFiltersCount}
onClick={onToggleFilters}
icon={<Filter className="w-4 h-4" />}
icon={<Settings className="w-4 h-4" />}
>
Filtres
</ToggleButton>
@@ -113,14 +117,16 @@ export function DesktopControls({
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={<Calendar className="w-4 h-4" />}
/>
title={kanbanFilters.showWithDueDate ? "Afficher toutes les tâches" : "Afficher seulement les tâches avec échéance"}
icon={<Clock className="w-4 h-4" />}
>
Échéance
</ToggleButton>
</ControlGroup>
</ControlSection>
{/* Section droite : Raccourcis + Bouton Nouvelle tâche */}
<ControlSection className="justify-between lg:justify-start">
<ControlSection className="justify-between lg:justify-start items-center">
<ControlGroup className="border-l border-[var(--border)] ml-2 pl-2 pr-4">
{/* Raccourcis Sources (Jira & TFS) */}
<IntegrationFilter
@@ -134,6 +140,7 @@ export function DesktopControls({
onClick={onToggleCompactView}
title={compactView ? "Vue détaillée" : "Vue compacte"}
icon={compactView ? <List className="w-4 h-4" /> : <Grid3X3 className="w-4 h-4" />}
className="h-[33px]"
>
{compactView ? 'Détaillée' : 'Compacte'}
</ToggleButton>
@@ -144,6 +151,7 @@ export function DesktopControls({
onClick={onToggleSwimlanes}
title={swimlanesByTags ? "Vue standard" : "Vue swimlanes"}
icon={swimlanesByTags ? <Layout className="w-4 h-4" /> : <Grid3X3 className="w-4 h-4" />}
className="h-[33px]"
>
{swimlanesByTags ? 'Standard' : 'Swimlanes'}
</ToggleButton>
@@ -155,9 +163,8 @@ export function DesktopControls({
{/* Bouton d'ajout de tâche */}
<Button
variant="primary"
size="sm"
onClick={onCreateTask}
className="flex items-center gap-2"
className="flex items-center gap-2 h-[34px] max-w-[180px] truncate"
>
<Plus className="w-4 h-4" />
Nouvelle tâche

View File

@@ -5,7 +5,7 @@ import { Button, ToggleButton, ControlPanel } from '@/components/ui';
import { IntegrationFilter } from '@/components/dashboard/IntegrationFilter';
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
import type { KanbanFilters } from '@/lib/types';
import { Menu, Plus, Filter, Target, List, Grid3X3 } from 'lucide-react';
import { Menu, Plus, Settings, Target, List, Grid3X3 } from 'lucide-react';
interface MobileControlsProps {
showFilters: boolean;
@@ -77,7 +77,7 @@ export function MobileControls({
onToggleFilters();
setIsMenuOpen(false);
}}
icon={<Filter className="w-4 h-4" />}
icon={<Settings className="w-4 h-4" />}
>
Filtres
</ToggleButton>

View File

@@ -9,7 +9,6 @@ import { MetricCard } from '@/components/ui/MetricCard';
import { AchievementCard } from '@/components/ui/AchievementCard';
import { ChallengeCard } from '@/components/ui/ChallengeCard';
import { SkeletonCard } from '@/components/ui/SkeletonCard';
import { RecentTaskTimeline } from '@/components/ui/RecentTaskTimeline';
import { AchievementData } from '@/components/ui/AchievementCard';
import { ChallengeData } from '@/components/ui/ChallengeCard';
@@ -226,52 +225,6 @@ export function CardsSection() {
</div>
</div>
{/* Recent Task Timeline */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">Recent Task Timeline</h3>
<div className="space-y-1">
<RecentTaskTimeline
title="Implement user authentication"
description="Add login and registration functionality with JWT tokens"
status="in_progress"
priority="high"
tags={['auth', 'security', 'backend']}
dueDate={new Date(Date.now() + 3 * 86400000)}
source="jira"
jiraKey="PROJ-123"
updatedAt={new Date(Date.now() - 2 * 3600000)}
/>
<RecentTaskTimeline
title="Design new dashboard"
description="Create a modern dashboard interface with analytics"
status="todo"
priority="medium"
tags={['design', 'ui', 'frontend']}
dueDate={new Date(Date.now() + 7 * 86400000)}
source="manual"
updatedAt={new Date(Date.now() - 1 * 86400000)}
/>
<RecentTaskTimeline
title="Fix critical bug in payment system"
description="Resolve issue with payment processing"
status="done"
priority="high"
tags={['bug', 'payment', 'critical']}
completedAt={new Date(Date.now() - 1 * 3600000)}
source="tfs"
tfsPullRequestId={456}
/>
<RecentTaskTimeline
title="Update documentation"
description="Update API documentation for new endpoints"
status="in_progress"
priority="low"
tags={['docs', 'api']}
source="reminders"
updatedAt={new Date(Date.now() - 30 * 60000)}
/>
</div>
</div>
{/* Skeleton Cards */}
<div className="space-y-4">

View File

@@ -42,6 +42,8 @@ interface DropdownProps {
className?: string;
/** Classe CSS additionnelle pour le contenu */
contentClassName?: string;
/** Classe CSS additionnelle pour le bouton trigger */
triggerClassName?: string;
/** Callback quand le dropdown s'ouvre */
onOpen?: () => void;
/** Callback quand le dropdown se ferme */
@@ -66,6 +68,7 @@ export function Dropdown({
zIndex = 9999,
className,
contentClassName,
triggerClassName,
onOpen,
onClose,
open: controlledOpen,
@@ -252,6 +255,7 @@ export function Dropdown({
className={cn(
'flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md transition-colors',
getVariantStyles(),
triggerClassName,
disabled && 'opacity-50 cursor-not-allowed'
)}
>

View File

@@ -22,6 +22,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
const { isConfigured: isJiraConfigured, config: jiraConfig } = useJiraConfig();
const pathname = usePathname();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [tabletMenuOpen, setTabletMenuOpen] = useState(false);
const [themeDropdownOpen, setThemeDropdownOpen] = useState(false);
const { openModal: openShortcutsModal } = useKeyboardShortcutsModal();
const { data: session } = useSession();
@@ -171,44 +172,97 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
</div>
{/* Auth controls à droite mobile */}
<div className="flex items-center gap-1">
{/* Auth controls à droite mobile - dans la ligne principale */}
<div className="hidden sm:block">
<AuthButton />
</div>
</div>
{/* Ligne Auth séparée sur très petits écrans */}
<div className="lg:hidden sm:hidden flex justify-end pt-2">
<AuthButton />
</div>
{/* Layout desktop - une seule ligne comme avant */}
<div className="hidden lg:flex items-center justify-between gap-6">
{/* Titre et status */}
<div className="flex items-center gap-6">
<div className="flex items-center gap-4 w-[300px]">
<div className="flex items-center gap-4 min-w-0">
<div className={`w-3 h-3 rounded-full shadow-lg ${
syncing
? 'bg-yellow-400 animate-spin shadow-yellow-400/50'
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
}`}></div>
<div>
<h1 className="text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider">
{title}
<div className="min-w-0">
<h1 className="text-xl xl:text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider truncate">
<span className="sm:hidden">{title}</span>
<span className="hidden sm:inline">{title}</span>
</h1>
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-sm">
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-xs sm:text-sm truncate">
{subtitle}
</p>
</div>
</div>
{/* Navigation desktop */}
<nav className="flex items-center gap-2">
{navLinks.map(({ href, label }) => (
<nav className="flex items-center gap-1 xl:gap-2 flex-wrap">
{navLinks.slice(0, 4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={getLinkClasses(href)}
className={`${getLinkClasses(href)} text-xs xl:text-sm`}
>
{label}
</Link>
))}
{/* Plus d'éléments sur très grands écrans */}
<div className="hidden 2xl:flex items-center gap-1">
{navLinks.slice(4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={getLinkClasses(href)}
>
{label}
</Link>
))}
</div>
{/* Menu dropdown pour écrans moyens */}
<div className="xl:hidden relative">
<button
onClick={() => setTabletMenuOpen(!tabletMenuOpen)}
className="font-mono text-xs uppercase tracking-wider transition-colors px-2 py-1.5 rounded-md text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]"
title="Plus de liens"
>
</button>
{tabletMenuOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-[200]"
onClick={() => setTabletMenuOpen(false)}
/>
{/* Menu items */}
<div className="absolute right-0 top-full mt-2 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[201] py-2 min-w-[119px]">
{navLinks.slice(4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={`block px-4 py-2 text-sm transition-colors ${getMobileLinkClasses(href)}`}
onClick={() => setTabletMenuOpen(false)}
>
{label}
</Link>
))}
</div>
</>
)}
</div>
{/* Keyboard Shortcuts desktop */}
<button
onClick={openShortcutsModal}
@@ -311,7 +365,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
className={getMobileLinkClasses('/profile')}
onClick={() => setMobileMenuOpen(false)}
>
👤 Profil
Profil
</Link>
{/* Bouton déconnexion */}
@@ -322,7 +376,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
}}
className="font-mono text-sm uppercase tracking-wider transition-colors px-4 py-3 rounded-md block w-full text-left text-[var(--destructive)] hover:text-[var(--destructive)] hover:bg-[var(--destructive)]/10"
>
🚪 Déconnexion
Déconnexion
</button>
</>
)}

View File

@@ -1,128 +0,0 @@
import { HTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
import { StatusBadge } from './StatusBadge';
import { TaskStatus } from '@/lib/types';
import { PriorityBadge } from './PriorityBadge';
import { TagDisplay } from './TagDisplay';
import { formatDateForDisplay } from '@/lib/date-utils';
import { ChevronRight } from 'lucide-react';
interface RecentTaskTimelineProps extends HTMLAttributes<HTMLDivElement> {
title: string;
description?: string;
tags?: string[];
priority?: 'low' | 'medium' | 'high';
status?: TaskStatus;
dueDate?: Date;
completedAt?: Date;
updatedAt?: Date;
source?: 'manual' | 'jira' | 'tfs' | 'reminders';
jiraKey?: string;
tfsPullRequestId?: number;
onClick?: () => void;
className?: string;
availableTags?: Array<{ id: string; name: string; color: string }>;
}
export function RecentTaskTimeline({
title,
description,
tags = [],
priority,
status = 'todo',
dueDate,
completedAt,
updatedAt,
source = 'manual',
jiraKey,
tfsPullRequestId,
onClick,
className,
availableTags = [],
...props
}: RecentTaskTimelineProps) {
const getSourceIcon = () => {
switch (source) {
case 'jira':
return <div className="w-2 h-2 bg-[var(--primary)] rounded-full" />;
case 'tfs':
return <div className="w-2 h-2 bg-[var(--blue)] rounded-full" />;
case 'reminders':
return <div className="w-2 h-2 bg-[var(--accent)] rounded-full" />;
default:
return <div className="w-2 h-2 bg-[var(--gray)] rounded-full" />;
}
};
const getTimeInfo = () => {
if (completedAt) {
return `Terminé ${formatDateForDisplay(completedAt)}`;
}
if (dueDate) {
return `Échéance ${formatDateForDisplay(dueDate)}`;
}
if (updatedAt) {
return `Modifié ${formatDateForDisplay(updatedAt)}`;
}
return null;
};
return (
<div
className={cn(
"group relative flex items-start gap-4 p-3 rounded-lg hover:bg-[var(--card-hover)] transition-colors cursor-pointer",
className
)}
onClick={onClick}
{...props}
>
{/* Timeline dot */}
<div className="flex-shrink-0 mt-2">
{getSourceIcon()}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-1">
<h4 className="font-medium text-[var(--foreground)] text-sm group-hover:text-[var(--primary)] transition-colors">
{title}
</h4>
<div className="flex items-center gap-1 flex-shrink-0">
{priority && <PriorityBadge priority={priority} />}
<StatusBadge status={status} />
</div>
</div>
{description && (
<p className="text-xs text-[var(--muted-foreground)] mb-2 line-clamp-1">
{description}
</p>
)}
{tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={tags}
availableTags={availableTags}
maxTags={2}
size="sm"
/>
</div>
)}
<div className="flex items-center justify-between text-xs text-[var(--muted-foreground)]">
<div className="flex items-center gap-2">
{jiraKey && <span className="font-mono text-[var(--primary)]">{jiraKey}</span>}
{tfsPullRequestId && <span className="font-mono text-[var(--blue)]">PR #{tfsPullRequestId}</span>}
</div>
<span>{getTimeInfo()}</span>
</div>
</div>
{/* Arrow indicator */}
<div className="flex-shrink-0 mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
<ChevronRight className="w-4 h-4 text-[var(--primary)]" />
</div>
</div>
);
}

View File

@@ -65,7 +65,7 @@ const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
value={localValue}
onChange={handleChange}
placeholder={placeholder}
className="bg-[var(--card)] border-[var(--border)] w-full"
className="bg-[var(--card)] border-[var(--border)] w-full h-[34px]"
{...props}
/>
</div>

View File

@@ -13,17 +13,17 @@ 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-[color-mix(in_srgb,var(--primary)_15%,transparent)] text-[var(--foreground)] border border-[var(--primary)]'
: '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',
? 'bg-[color-mix(in_srgb,var(--primary)_15%,transparent)] text-[var(--foreground)] border border-[var(--primary)]'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--primary)]/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',
? 'bg-[color-mix(in_srgb,var(--primary)_15%,transparent)] text-[var(--foreground)] border border-[var(--primary)]'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--primary)]/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'
@@ -33,8 +33,8 @@ const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(
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'
sm: isIconOnly ? 'px-2 py-[5px] text-sm h-[34px]' : 'px-3 py-[5px] text-sm h-[34px]',
md: isIconOnly ? 'px-2 py-[5px] text-sm h-[34px]' : 'px-3 py-[5px] text-sm h-[34px]'
};
return (

View File

@@ -11,7 +11,6 @@ export { StatCard } from './StatCard';
export { ProgressBar } from './ProgressBar';
export { ActionCard } from './ActionCard';
export { TaskCard } from './TaskCard';
export { RecentTaskTimeline } from './RecentTaskTimeline';
export { MetricCard } from './MetricCard';
// Composants Kanban