refactor: update theme management and enhance UI components
- Refactored theme imports in `preferences.ts` and `ThemeSelector.tsx` to use centralized `theme-config`. - Added new CSS variables for special cards in `globals.css` to improve theme consistency. - Enhanced `Header` and `TaskCard` components with theme dropdown functionality for better user experience. - Updated `ThemeProvider` to support cycling through dark themes, improving theme selection flexibility. - Cleaned up unused imports and streamlined component structures for better maintainability.
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { userPreferencesService } from '@/services/core/user-preferences';
|
import { userPreferencesService } from '@/services/core/user-preferences';
|
||||||
import { KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus, Theme } from '@/lib/types';
|
import { KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types';
|
||||||
|
import { Theme } from '@/lib/theme-config';
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,6 +22,14 @@
|
|||||||
--blue: #2563eb; /* blue-600 */
|
--blue: #2563eb; /* blue-600 */
|
||||||
--gray: #6b7280; /* gray-500 */
|
--gray: #6b7280; /* gray-500 */
|
||||||
--gray-light: #e5e7eb; /* gray-200 */
|
--gray-light: #e5e7eb; /* gray-200 */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #dbeafe; /* blue-100 - clair */
|
||||||
|
--tfs-card: #fed7aa; /* orange-200 - clair */
|
||||||
|
--jira-border: #3b82f6; /* blue-500 */
|
||||||
|
--tfs-border: #f59e0b; /* amber-500 */
|
||||||
|
--jira-text: #1e40af; /* blue-800 - foncé pour contraste */
|
||||||
|
--tfs-text: #92400e; /* amber-800 - foncé pour contraste */
|
||||||
}
|
}
|
||||||
|
|
||||||
.light {
|
.light {
|
||||||
@@ -46,13 +54,21 @@
|
|||||||
--blue: #2563eb; /* blue-600 */
|
--blue: #2563eb; /* blue-600 */
|
||||||
--gray: #6b7280; /* gray-500 */
|
--gray: #6b7280; /* gray-500 */
|
||||||
--gray-light: #e5e7eb; /* gray-200 */
|
--gray-light: #e5e7eb; /* gray-200 */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #dbeafe; /* blue-100 - clair */
|
||||||
|
--tfs-card: #fed7aa; /* orange-200 - clair */
|
||||||
|
--jira-border: #3b82f6; /* blue-500 */
|
||||||
|
--tfs-border: #f59e0b; /* amber-500 */
|
||||||
|
--jira-text: #1e40af; /* blue-800 - foncé pour contraste */
|
||||||
|
--tfs-text: #92400e; /* amber-800 - foncé pour contraste */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
/* Dark theme override */
|
/* Dark theme override */
|
||||||
--background: #1e293b; /* slate-800 - encore plus clair */
|
--background: #1e293b; /* slate-800 - encore plus clair */
|
||||||
--foreground: #f1f5f9; /* slate-100 */
|
--foreground: #f1f5f9; /* slate-100 */
|
||||||
--card: #334155; /* slate-700 - beaucoup plus clair pour contraste fort */
|
--card: #1e293b; /* slate-800 - même couleur que le background */
|
||||||
--card-hover: #475569; /* slate-600 */
|
--card-hover: #475569; /* slate-600 */
|
||||||
--card-column: #0f172a; /* slate-900 - plus foncé que les cartes */
|
--card-column: #0f172a; /* slate-900 - plus foncé que les cartes */
|
||||||
--border: #64748b; /* slate-500 - encore plus clair */
|
--border: #64748b; /* slate-500 - encore plus clair */
|
||||||
@@ -70,6 +86,14 @@
|
|||||||
--blue: #3b82f6; /* blue-500 */
|
--blue: #3b82f6; /* blue-500 */
|
||||||
--gray: #9ca3af; /* gray-400 */
|
--gray: #9ca3af; /* gray-400 */
|
||||||
--gray-light: #374151; /* gray-700 */
|
--gray-light: #374151; /* gray-700 */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #1e3a8a; /* blue-900 - très foncé */
|
||||||
|
--tfs-card: #9a3412; /* orange-900 - très foncé */
|
||||||
|
--jira-border: #60a5fa; /* blue-400 - plus clair pour contraste */
|
||||||
|
--tfs-border: #fb923c; /* orange-400 - plus clair pour contraste */
|
||||||
|
--jira-text: #93c5fd; /* blue-300 - clair pour contraste */
|
||||||
|
--tfs-text: #fdba74; /* orange-300 - clair pour contraste */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dracula {
|
.dracula {
|
||||||
@@ -94,6 +118,14 @@
|
|||||||
--blue: #8be9fd; /* dracula cyan */
|
--blue: #8be9fd; /* dracula cyan */
|
||||||
--gray: #6272a4; /* dracula comment */
|
--gray: #6272a4; /* dracula comment */
|
||||||
--gray-light: #44475a; /* dracula current line */
|
--gray-light: #44475a; /* dracula current line */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #44475a; /* dracula current line - fond neutre */
|
||||||
|
--tfs-card: #44475a; /* dracula current line - fond neutre */
|
||||||
|
--jira-border: #8be9fd; /* dracula cyan */
|
||||||
|
--tfs-border: #ffb86c; /* dracula orange */
|
||||||
|
--jira-text: #f8f8f2; /* dracula foreground - texte principal */
|
||||||
|
--tfs-text: #f8f8f2; /* dracula foreground - texte principal */
|
||||||
}
|
}
|
||||||
|
|
||||||
.monokai {
|
.monokai {
|
||||||
@@ -118,6 +150,14 @@
|
|||||||
--blue: #66d9ef; /* monokai cyan */
|
--blue: #66d9ef; /* monokai cyan */
|
||||||
--gray: #75715e; /* monokai comment */
|
--gray: #75715e; /* monokai comment */
|
||||||
--gray-light: #3e3d32; /* monokai selection */
|
--gray-light: #3e3d32; /* monokai selection */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #3e3d32; /* monokai selection - fond neutre */
|
||||||
|
--tfs-card: #3e3d32; /* monokai selection - fond neutre */
|
||||||
|
--jira-border: #66d9ef; /* monokai cyan */
|
||||||
|
--tfs-border: #fd971f; /* monokai orange */
|
||||||
|
--jira-text: #f8f8f2; /* monokai foreground */
|
||||||
|
--tfs-text: #f8f8f2; /* monokai foreground */
|
||||||
}
|
}
|
||||||
|
|
||||||
.nord {
|
.nord {
|
||||||
@@ -142,6 +182,14 @@
|
|||||||
--blue: #5e81ac; /* nord10 */
|
--blue: #5e81ac; /* nord10 */
|
||||||
--gray: #4c566a; /* nord3 */
|
--gray: #4c566a; /* nord3 */
|
||||||
--gray-light: #3b4252; /* nord1 */
|
--gray-light: #3b4252; /* nord1 */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #3b4252; /* nord1 - fond neutre */
|
||||||
|
--tfs-card: #3b4252; /* nord1 - fond neutre */
|
||||||
|
--jira-border: #5e81ac; /* nord10 - bleu */
|
||||||
|
--tfs-border: #d08770; /* nord12 - orange */
|
||||||
|
--jira-text: #d8dee9; /* nord4 - texte principal */
|
||||||
|
--tfs-text: #d8dee9; /* nord4 - texte principal */
|
||||||
}
|
}
|
||||||
|
|
||||||
.gruvbox {
|
.gruvbox {
|
||||||
@@ -166,6 +214,14 @@
|
|||||||
--blue: #83a598; /* gruvbox blue */
|
--blue: #83a598; /* gruvbox blue */
|
||||||
--gray: #a89984; /* gruvbox gray */
|
--gray: #a89984; /* gruvbox gray */
|
||||||
--gray-light: #3c3836; /* gruvbox bg1 */
|
--gray-light: #3c3836; /* gruvbox bg1 */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #3c3836; /* gruvbox bg1 - fond neutre */
|
||||||
|
--tfs-card: #3c3836; /* gruvbox bg1 - fond neutre */
|
||||||
|
--jira-border: #83a598; /* gruvbox blue */
|
||||||
|
--tfs-border: #fe8019; /* gruvbox orange */
|
||||||
|
--jira-text: #ebdbb2; /* gruvbox fg */
|
||||||
|
--tfs-text: #ebdbb2; /* gruvbox fg */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tokyo_night {
|
.tokyo_night {
|
||||||
@@ -190,6 +246,14 @@
|
|||||||
--blue: #7aa2f7; /* tokyo-night blue */
|
--blue: #7aa2f7; /* tokyo-night blue */
|
||||||
--gray: #565f89; /* tokyo-night comment */
|
--gray: #565f89; /* tokyo-night comment */
|
||||||
--gray-light: #24283b; /* tokyo-night bg_highlight */
|
--gray-light: #24283b; /* tokyo-night bg_highlight */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #24283b; /* tokyo-night bg_highlight - fond neutre */
|
||||||
|
--tfs-card: #24283b; /* tokyo-night bg_highlight - fond neutre */
|
||||||
|
--jira-border: #7aa2f7; /* tokyo-night blue */
|
||||||
|
--tfs-border: #ff9e64; /* tokyo-night orange */
|
||||||
|
--jira-text: #a9b1d6; /* tokyo-night fg */
|
||||||
|
--tfs-text: #a9b1d6; /* tokyo-night fg */
|
||||||
}
|
}
|
||||||
|
|
||||||
.catppuccin {
|
.catppuccin {
|
||||||
@@ -214,6 +278,14 @@
|
|||||||
--blue: #89b4fa; /* catppuccin blue */
|
--blue: #89b4fa; /* catppuccin blue */
|
||||||
--gray: #6c7086; /* catppuccin overlay0 */
|
--gray: #6c7086; /* catppuccin overlay0 */
|
||||||
--gray-light: #313244; /* catppuccin surface0 */
|
--gray-light: #313244; /* catppuccin surface0 */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #313244; /* catppuccin surface0 - fond neutre */
|
||||||
|
--tfs-card: #313244; /* catppuccin surface0 - fond neutre */
|
||||||
|
--jira-border: #89b4fa; /* catppuccin blue */
|
||||||
|
--tfs-border: #fab387; /* catppuccin peach */
|
||||||
|
--jira-text: #cdd6f4; /* catppuccin text */
|
||||||
|
--tfs-text: #cdd6f4; /* catppuccin text */
|
||||||
}
|
}
|
||||||
|
|
||||||
.rose_pine {
|
.rose_pine {
|
||||||
@@ -238,6 +310,14 @@
|
|||||||
--blue: #3e8fb0; /* rose-pine pine */
|
--blue: #3e8fb0; /* rose-pine pine */
|
||||||
--gray: #6e6a86; /* rose-pine muted */
|
--gray: #6e6a86; /* rose-pine muted */
|
||||||
--gray-light: #26233a; /* rose-pine surface */
|
--gray-light: #26233a; /* rose-pine surface */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #26233a; /* rose-pine surface - fond neutre */
|
||||||
|
--tfs-card: #26233a; /* rose-pine surface - fond neutre */
|
||||||
|
--jira-border: #3e8fb0; /* rose-pine pine - bleu */
|
||||||
|
--tfs-border: #f6c177; /* rose-pine gold - orange/jaune */
|
||||||
|
--jira-text: #e0def4; /* rose-pine text */
|
||||||
|
--tfs-text: #e0def4; /* rose-pine text */
|
||||||
}
|
}
|
||||||
|
|
||||||
.one_dark {
|
.one_dark {
|
||||||
@@ -262,6 +342,14 @@
|
|||||||
--blue: #61afef; /* one-dark blue */
|
--blue: #61afef; /* one-dark blue */
|
||||||
--gray: #5c6370; /* one-dark bg3 */
|
--gray: #5c6370; /* one-dark bg3 */
|
||||||
--gray-light: #3e4451; /* one-dark bg1 */
|
--gray-light: #3e4451; /* one-dark bg1 */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #3e4451; /* one-dark bg1 - fond neutre */
|
||||||
|
--tfs-card: #3e4451; /* one-dark bg1 - fond neutre */
|
||||||
|
--jira-border: #61afef; /* one-dark blue */
|
||||||
|
--tfs-border: #e5c07b; /* one-dark yellow */
|
||||||
|
--jira-text: #abb2bf; /* one-dark fg */
|
||||||
|
--tfs-text: #abb2bf; /* one-dark fg */
|
||||||
}
|
}
|
||||||
|
|
||||||
.material {
|
.material {
|
||||||
@@ -286,6 +374,14 @@
|
|||||||
--blue: #2196f3; /* material info */
|
--blue: #2196f3; /* material info */
|
||||||
--gray: #3c3c3c; /* material outline */
|
--gray: #3c3c3c; /* material outline */
|
||||||
--gray-light: #1e1e1e; /* material surface */
|
--gray-light: #1e1e1e; /* material surface */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #1e1e1e; /* material surface - fond neutre */
|
||||||
|
--tfs-card: #1e1e1e; /* material surface - fond neutre */
|
||||||
|
--jira-border: #2196f3; /* material info - bleu */
|
||||||
|
--tfs-border: #ffab40; /* material secondary - orange */
|
||||||
|
--jira-text: #ffffff; /* material on-bg */
|
||||||
|
--tfs-text: #ffffff; /* material on-bg */
|
||||||
}
|
}
|
||||||
|
|
||||||
.solarized {
|
.solarized {
|
||||||
@@ -310,6 +406,14 @@
|
|||||||
--blue: #268bd2; /* solarized blue */
|
--blue: #268bd2; /* solarized blue */
|
||||||
--gray: #586e75; /* solarized base01 */
|
--gray: #586e75; /* solarized base01 */
|
||||||
--gray-light: #073642; /* solarized base02 */
|
--gray-light: #073642; /* solarized base02 */
|
||||||
|
|
||||||
|
/* Cartes spéciales */
|
||||||
|
--jira-card: #073642; /* solarized base02 - fond neutre */
|
||||||
|
--tfs-card: #073642; /* solarized base02 - fond neutre */
|
||||||
|
--jira-border: #268bd2; /* solarized blue */
|
||||||
|
--tfs-border: #b58900; /* solarized yellow */
|
||||||
|
--jira-text: #93a1a1; /* solarized base1 */
|
||||||
|
--tfs-text: #93a1a1; /* solarized base1 */
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ThemeProvider } from "@/contexts/ThemeContext";
|
|||||||
import { JiraConfigProvider } from "@/contexts/JiraConfigContext";
|
import { JiraConfigProvider } from "@/contexts/JiraConfigContext";
|
||||||
import { UserPreferencesProvider } from "@/contexts/UserPreferencesContext";
|
import { UserPreferencesProvider } from "@/contexts/UserPreferencesContext";
|
||||||
import { userPreferencesService } from "@/services/core/user-preferences";
|
import { userPreferencesService } from "@/services/core/user-preferences";
|
||||||
|
import { KeyboardShortcuts } from "@/components/KeyboardShortcuts";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -38,6 +39,7 @@ export default async function RootLayout({
|
|||||||
initialTheme={initialPreferences.viewPreferences.theme}
|
initialTheme={initialPreferences.viewPreferences.theme}
|
||||||
userPreferredTheme={initialPreferences.viewPreferences.theme === 'light' ? 'dark' : initialPreferences.viewPreferences.theme}
|
userPreferredTheme={initialPreferences.viewPreferences.theme === 'light' ? 'dark' : initialPreferences.viewPreferences.theme}
|
||||||
>
|
>
|
||||||
|
<KeyboardShortcuts />
|
||||||
<JiraConfigProvider config={initialPreferences.jiraConfig}>
|
<JiraConfigProvider config={initialPreferences.jiraConfig}>
|
||||||
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
8
src/components/KeyboardShortcuts.tsx
Normal file
8
src/components/KeyboardShortcuts.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
||||||
|
|
||||||
|
export function KeyboardShortcuts() {
|
||||||
|
useKeyboardShortcuts();
|
||||||
|
return null; // Ce composant ne rend rien, il gère juste les raccourcis
|
||||||
|
}
|
||||||
@@ -1,23 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme } from '@/contexts/ThemeContext';
|
||||||
import { Theme } from '@/lib/types';
|
import { Theme } from '@/lib/theme-config';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { THEME_CONFIG, getThemeMetadata } from '@/lib/theme-config';
|
||||||
|
|
||||||
|
|
||||||
const themes: { id: Theme; name: string; description: string }[] = [
|
// Génération des thèmes à partir de la configuration centralisée
|
||||||
{ id: 'dark', name: 'Dark', description: 'Thème sombre par défaut' },
|
const themes: { id: Theme; name: string; description: string }[] = THEME_CONFIG.allThemes.map(themeId => {
|
||||||
{ id: 'dracula', name: 'Dracula', description: 'Thème Dracula coloré' },
|
const metadata = getThemeMetadata(themeId);
|
||||||
{ id: 'monokai', name: 'Monokai', description: 'Thème Monokai vibrant' },
|
return {
|
||||||
{ id: 'nord', name: 'Nord', description: 'Thème Nord minimaliste' },
|
id: themeId,
|
||||||
{ id: 'gruvbox', name: 'Gruvbox', description: 'Thème Gruvbox chaleureux' },
|
name: metadata.name,
|
||||||
{ id: 'tokyo_night', name: 'Tokyo Night', description: 'Thème Tokyo Night moderne' },
|
description: metadata.description
|
||||||
{ id: 'catppuccin', name: 'Catppuccin', description: 'Thème Catppuccin pastel' },
|
};
|
||||||
{ id: 'rose_pine', name: 'Rose Pine', description: 'Thème Rose Pine élégant' },
|
});
|
||||||
{ id: 'one_dark', name: 'One Dark', description: 'Thème One Dark populaire' },
|
|
||||||
{ id: 'material', name: 'Material', description: 'Thème Material Design' },
|
|
||||||
{ id: 'solarized', name: 'Solarized', description: 'Thème Solarized scientifique' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Composant pour l'aperçu du thème
|
// Composant pour l'aperçu du thème
|
||||||
function ThemePreview({ themeId, isSelected }: { themeId: Theme; isSelected: boolean }) {
|
function ThemePreview({ themeId, isSelected }: { themeId: Theme; isSelected: boolean }) {
|
||||||
|
|||||||
@@ -2,12 +2,8 @@
|
|||||||
|
|
||||||
import { Task } from '@/lib/types';
|
import { Task } from '@/lib/types';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { TagDisplay } from '@/components/ui/TagDisplay';
|
|
||||||
import { formatDateShort } from '@/lib/date-utils';
|
|
||||||
import { TaskCard } from '@/components/ui';
|
import { TaskCard } from '@/components/ui';
|
||||||
import { useTasksContext } from '@/contexts/TasksContext';
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
import { getPriorityConfig, getStatusLabel } from '@/lib/status-config';
|
|
||||||
import { TaskPriority } from '@/lib/types';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
interface RecentTasksProps {
|
interface RecentTasksProps {
|
||||||
@@ -45,46 +41,44 @@ export function RecentTasks({ tasks }: RecentTasksProps) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{recentTasks.map((task) => (
|
{recentTasks.map((task) => (
|
||||||
<TaskCard
|
<div key={task.id} className="relative group">
|
||||||
key={task.id}
|
<TaskCard
|
||||||
title={task.title}
|
variant="detailed"
|
||||||
description={task.description}
|
source={task.source || 'manual'}
|
||||||
status={getStatusLabel(task.status)}
|
title={task.title}
|
||||||
priority={task.priority ? (() => {
|
description={task.description}
|
||||||
try {
|
status={task.status}
|
||||||
return getPriorityConfig(task.priority as TaskPriority).label;
|
priority={task.priority as 'low' | 'medium' | 'high' | 'urgent'}
|
||||||
} catch {
|
tags={task.tags || []}
|
||||||
return task.priority;
|
dueDate={task.dueDate}
|
||||||
}
|
completedAt={task.completedAt}
|
||||||
})() : undefined}
|
jiraKey={task.jiraKey}
|
||||||
tags={task.tags && task.tags.length > 0 ? [
|
jiraProject={task.jiraProject}
|
||||||
<TagDisplay
|
jiraType={task.jiraType}
|
||||||
key="tags"
|
tfsPullRequestId={task.tfsPullRequestId}
|
||||||
tags={task.tags.slice(0, 2)}
|
tfsProject={task.tfsProject}
|
||||||
availableTags={availableTags}
|
tfsRepository={task.tfsRepository}
|
||||||
size="sm"
|
availableTags={availableTags}
|
||||||
maxTags={2}
|
fontSize="small"
|
||||||
showColors={true}
|
onTitleClick={() => {
|
||||||
/>,
|
// Navigation vers le kanban avec la tâche sélectionnée
|
||||||
...(task.tags.length > 2 ? [
|
window.location.href = `/kanban?taskId=${task.id}`;
|
||||||
<span key="more" className="text-xs text-[var(--muted-foreground)]">
|
}}
|
||||||
+{task.tags.length - 2}
|
/>
|
||||||
</span>
|
|
||||||
] : [])
|
{/* Overlay avec lien vers le kanban */}
|
||||||
] : undefined}
|
<Link
|
||||||
metadata={formatDateShort(task.updatedAt)}
|
href={`/kanban?taskId=${task.id}`}
|
||||||
actions={
|
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200 bg-[var(--primary)]/5 rounded-lg flex items-center justify-center"
|
||||||
<Link
|
title="Ouvrir dans le Kanban"
|
||||||
href={`/kanban?taskId=${task.id}`}
|
>
|
||||||
className="p-1 rounded hover:bg-[var(--muted)]/50 transition-colors"
|
<div className="bg-[var(--primary)]/20 backdrop-blur-sm rounded-full p-2 border border-[var(--primary)]/30">
|
||||||
title="Ouvrir dans le Kanban"
|
<svg className="w-4 h-4 text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
>
|
|
||||||
<svg className="w-3 h-3 text-[var(--primary)] hover:text-[var(--primary)]/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</div>
|
||||||
}
|
</Link>
|
||||||
/>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,41 +9,21 @@ 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, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup, FilterSummary, FilterChip, ColumnHeader, EmptyState, DropZone } from '@/components/ui';
|
import { StatCard, ProgressBar, ActionCard, TaskCard, MetricCard, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup, FilterSummary, FilterChip, ColumnHeader, EmptyState, DropZone } from '@/components/ui';
|
||||||
import { ThemeSelector } from '@/components/ThemeSelector';
|
import { Header } from '@/components/ui/Header';
|
||||||
|
|
||||||
export function UIShowcaseClient() {
|
export function UIShowcaseClient() {
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[var(--background)] p-8">
|
<div className="min-h-screen bg-[var(--background)]">
|
||||||
<div className="max-w-6xl mx-auto space-y-16">
|
{/* Header avec navigation et dropdown de thèmes */}
|
||||||
{/* Header */}
|
<Header
|
||||||
<div className="text-center space-y-6">
|
title="🎨 UI Showcase"
|
||||||
<h1 className="text-4xl font-mono font-bold text-[var(--foreground)]">
|
subtitle="Démonstration de tous les composants UI disponibles"
|
||||||
🎨 UI Components Showcase
|
/>
|
||||||
</h1>
|
|
||||||
<p className="text-lg text-[var(--muted-foreground)]">
|
|
||||||
Démonstration de tous les composants UI disponibles
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Theme Selector */}
|
<div className="max-w-6xl mx-auto p-8 space-y-16">
|
||||||
<section className="space-y-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] mb-6">
|
|
||||||
🎨 Sélecteur de Thèmes
|
|
||||||
</h2>
|
|
||||||
<p className="text-[var(--muted-foreground)] mb-8">
|
|
||||||
Changez de thème pour voir comment tous les composants s'adaptent
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<div className="bg-[var(--card)]/30 border border-[var(--border)]/50 rounded-lg p-6 backdrop-blur-sm">
|
|
||||||
<ThemeSelector />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Buttons Section */}
|
{/* Buttons Section */}
|
||||||
<section className="space-y-8">
|
<section className="space-y-8">
|
||||||
@@ -530,43 +510,145 @@ export function UIShowcaseClient() {
|
|||||||
{/* Task Cards */}
|
{/* Task Cards */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h3 className="text-lg font-medium text-[var(--foreground)]">Task Cards</h3>
|
<h3 className="text-lg font-medium text-[var(--foreground)]">Task Cards</h3>
|
||||||
<div className="space-y-3 max-w-2xl">
|
<div className="space-y-4 max-w-2xl">
|
||||||
<TaskCard
|
{/* Task Card Compacte */}
|
||||||
title="Refactoriser le système de thèmes"
|
<div className="space-y-2">
|
||||||
description="Améliorer la gestion des thèmes avec CSS variables"
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
status="En Cours"
|
variant="compact" - Vue compacte
|
||||||
priority="Haute"
|
</div>
|
||||||
tags={[
|
<TaskCard
|
||||||
<Badge key="tag1" variant="primary" className="text-xs">Frontend</Badge>,
|
variant="compact"
|
||||||
<Badge key="tag2" variant="success" className="text-xs">UI</Badge>
|
title="🎨 Refactoriser le système de thèmes"
|
||||||
]}
|
description="Améliorer la gestion des thèmes avec CSS variables"
|
||||||
metadata="Il y a 2h"
|
status="in_progress"
|
||||||
actions={
|
priority="high"
|
||||||
<button className="p-1 rounded hover:bg-[var(--muted)]/50 transition-colors">
|
tags={["Frontend", "UI"]}
|
||||||
<svg className="w-3 h-3 text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
source="manual"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
fontSize="medium"
|
||||||
</svg>
|
availableTags={[
|
||||||
</button>
|
{ id: "1", name: "Frontend", color: "#3b82f6" },
|
||||||
}
|
{ id: "2", name: "UI", color: "#10b981" }
|
||||||
/>
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TaskCard
|
{/* Task Card Détaillée */}
|
||||||
title="Créer les composants UI réutilisables"
|
<div className="space-y-2">
|
||||||
description="Développer une bibliothèque de composants cohérente"
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
status="Terminé"
|
variant="detailed" - Vue détaillée
|
||||||
priority="Moyenne"
|
</div>
|
||||||
tags={[
|
<TaskCard
|
||||||
<Badge key="tag1" variant="accent" className="text-xs">Design</Badge>
|
variant="detailed"
|
||||||
]}
|
title="🚀 Créer les composants UI réutilisables"
|
||||||
metadata="Hier"
|
description="Développer une bibliothèque de composants cohérente et maintenable"
|
||||||
actions={
|
status="done"
|
||||||
<button className="p-1 rounded hover:bg-[var(--muted)]/50 transition-colors">
|
priority="medium"
|
||||||
<svg className="w-3 h-3 text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
tags={["Design", "Components"]}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
source="manual"
|
||||||
</svg>
|
dueDate={new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)}
|
||||||
</button>
|
fontSize="medium"
|
||||||
}
|
availableTags={[
|
||||||
/>
|
{ id: "1", name: "Design", color: "#f59e0b" },
|
||||||
|
{ id: "2", name: "Components", color: "#8b5cf6" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Task Card Jira */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
source="jira" - Tâche Jira avec styles spéciaux
|
||||||
|
</div>
|
||||||
|
<TaskCard
|
||||||
|
variant="detailed"
|
||||||
|
title="🐛 Corriger le bug de synchronisation"
|
||||||
|
description="Résoudre le problème de synchronisation des données entre les services"
|
||||||
|
status="todo"
|
||||||
|
priority="urgent"
|
||||||
|
tags={["Bug", "Backend"]}
|
||||||
|
source="jira"
|
||||||
|
jiraKey="PROJ-123"
|
||||||
|
jiraProject="PROJ"
|
||||||
|
jiraType="Bug"
|
||||||
|
dueDate={new Date(Date.now() + 3 * 24 * 60 * 60 * 1000)}
|
||||||
|
fontSize="medium"
|
||||||
|
availableTags={[
|
||||||
|
{ id: "1", name: "Bug", color: "#ef4444" },
|
||||||
|
{ id: "2", name: "Backend", color: "#6b7280" }
|
||||||
|
]}
|
||||||
|
jiraConfig={{ baseUrl: "https://company.atlassian.net" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Task Card TFS */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
source="tfs" - Tâche TFS avec styles spéciaux
|
||||||
|
</div>
|
||||||
|
<TaskCard
|
||||||
|
variant="detailed"
|
||||||
|
title="📦 Implémenter la nouvelle API"
|
||||||
|
description="Développer l'API REST pour la gestion des utilisateurs"
|
||||||
|
status="review"
|
||||||
|
priority="high"
|
||||||
|
tags={["API", "Backend"]}
|
||||||
|
source="tfs"
|
||||||
|
tfsPullRequestId={456}
|
||||||
|
tfsProject="MyProject"
|
||||||
|
tfsRepository="backend-api"
|
||||||
|
fontSize="medium"
|
||||||
|
availableTags={[
|
||||||
|
{ id: "1", name: "API", color: "#06b6d4" },
|
||||||
|
{ id: "2", name: "Backend", color: "#6b7280" }
|
||||||
|
]}
|
||||||
|
tfsConfig={{ organizationUrl: "https://dev.azure.com/company" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Task Card avec différents statuts */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
Statuts spéciaux - freeze, archived, cancelled
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
|
<TaskCard
|
||||||
|
variant="compact"
|
||||||
|
title="❄️ Tâche gelée"
|
||||||
|
status="freeze"
|
||||||
|
priority="low"
|
||||||
|
tags={["Maintenance"]}
|
||||||
|
source="manual"
|
||||||
|
fontSize="small"
|
||||||
|
availableTags={[
|
||||||
|
{ id: "1", name: "Maintenance", color: "#64748b" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<TaskCard
|
||||||
|
variant="compact"
|
||||||
|
title="📁 Tâche archivée"
|
||||||
|
status="archived"
|
||||||
|
priority="medium"
|
||||||
|
tags={["Archive"]}
|
||||||
|
source="manual"
|
||||||
|
fontSize="small"
|
||||||
|
availableTags={[
|
||||||
|
{ id: "1", name: "Archive", color: "#6b7280" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<TaskCard
|
||||||
|
variant="compact"
|
||||||
|
title="❌ Tâche annulée"
|
||||||
|
status="cancelled"
|
||||||
|
priority="high"
|
||||||
|
tags={["Cancelled"]}
|
||||||
|
source="manual"
|
||||||
|
fontSize="small"
|
||||||
|
availableTags={[
|
||||||
|
{ id: "1", name: "Cancelled", color: "#ef4444" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { useJiraConfig } from '@/contexts/JiraConfigContext';
|
|||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { Theme } from '@/lib/theme-config';
|
||||||
|
import { THEME_CONFIG, getThemeMetadata } from '@/lib/theme-config';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -13,10 +15,22 @@ interface HeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Header({ title = "TowerControl", subtitle = "Task Management", syncing = false }: HeaderProps) {
|
export function Header({ title = "TowerControl", subtitle = "Task Management", syncing = false }: HeaderProps) {
|
||||||
const { theme, toggleTheme, userPreferredTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const { isConfigured: isJiraConfigured, config: jiraConfig } = useJiraConfig();
|
const { isConfigured: isJiraConfigured, config: jiraConfig } = useJiraConfig();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [themeDropdownOpen, setThemeDropdownOpen] = useState(false);
|
||||||
|
|
||||||
|
// Liste des thèmes disponibles avec leurs labels et icônes
|
||||||
|
const themes: { value: Theme; label: string; icon: string }[] = THEME_CONFIG.allThemes.map(themeValue => {
|
||||||
|
const metadata = getThemeMetadata(themeValue);
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: themeValue,
|
||||||
|
label: metadata.name,
|
||||||
|
icon: metadata.icon
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Fonction pour déterminer si un lien est actif
|
// Fonction pour déterminer si un lien est actif
|
||||||
const isActiveLink = (href: string) => {
|
const isActiveLink = (href: string) => {
|
||||||
@@ -83,24 +97,53 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
|||||||
|
|
||||||
{/* Controls mobile/tablette */}
|
{/* Controls mobile/tablette */}
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
{/* Theme Toggle */}
|
{/* Theme Dropdown */}
|
||||||
<button
|
<div className="relative">
|
||||||
onClick={toggleTheme}
|
<button
|
||||||
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
|
onClick={() => setThemeDropdownOpen(!themeDropdownOpen)}
|
||||||
title={theme === 'light' ? `Switch to ${userPreferredTheme} theme` : 'Switch to light theme'}
|
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
|
||||||
>
|
title="Select theme"
|
||||||
{theme === 'light' ? (
|
>
|
||||||
// Soleil pour le thème clair
|
{themes.find(t => t.value === theme)?.icon || '🎨'}
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
</button>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
||||||
</svg>
|
{themeDropdownOpen && (
|
||||||
) : (
|
<>
|
||||||
// Lune pour tous les thèmes sombres
|
{/* Backdrop */}
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
className="fixed inset-0 z-[200]"
|
||||||
</svg>
|
onClick={() => setThemeDropdownOpen(false)}
|
||||||
|
/>
|
||||||
|
{/* Dropdown */}
|
||||||
|
<div className="absolute right-0 top-full mt-2 w-48 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[201] overflow-hidden">
|
||||||
|
<div className="py-2">
|
||||||
|
{themes.map((themeOption) => (
|
||||||
|
<button
|
||||||
|
key={themeOption.value}
|
||||||
|
onClick={() => {
|
||||||
|
setTheme(themeOption.value);
|
||||||
|
setThemeDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className={`w-full text-left px-4 py-2 text-sm transition-colors flex items-center gap-3 ${
|
||||||
|
theme === themeOption.value
|
||||||
|
? 'text-[var(--primary)] bg-[var(--primary)]/10'
|
||||||
|
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--card-hover)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-base">{themeOption.icon}</span>
|
||||||
|
<span className="font-mono">{themeOption.label}</span>
|
||||||
|
{theme === themeOption.value && (
|
||||||
|
<svg className="w-4 h-4 ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
{/* Menu burger */}
|
{/* Menu burger */}
|
||||||
<button
|
<button
|
||||||
@@ -154,22 +197,53 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Theme Toggle desktop */}
|
{/* Theme Dropdown desktop */}
|
||||||
<button
|
<div className="relative">
|
||||||
onClick={toggleTheme}
|
<button
|
||||||
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-1 rounded-md hover:bg-[var(--card-hover)]"
|
onClick={() => setThemeDropdownOpen(!themeDropdownOpen)}
|
||||||
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
|
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-1 rounded-md hover:bg-[var(--card-hover)]"
|
||||||
>
|
title="Select theme"
|
||||||
{theme === 'dark' ? (
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
{themes.find(t => t.value === theme)?.icon || '🎨'}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
</button>
|
||||||
</svg>
|
|
||||||
) : (
|
{themeDropdownOpen && (
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
{/* Backdrop */}
|
||||||
</svg>
|
<div
|
||||||
|
className="fixed inset-0 z-[200]"
|
||||||
|
onClick={() => setThemeDropdownOpen(false)}
|
||||||
|
/>
|
||||||
|
{/* Dropdown */}
|
||||||
|
<div className="absolute right-0 top-full mt-2 w-48 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[201] overflow-hidden">
|
||||||
|
<div className="py-2">
|
||||||
|
{themes.map((themeOption) => (
|
||||||
|
<button
|
||||||
|
key={themeOption.value}
|
||||||
|
onClick={() => {
|
||||||
|
setTheme(themeOption.value);
|
||||||
|
setThemeDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className={`w-full text-left px-4 py-2 text-sm transition-colors flex items-center gap-3 ${
|
||||||
|
theme === themeOption.value
|
||||||
|
? 'text-[var(--primary)] bg-[var(--primary)]/10'
|
||||||
|
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--card-hover)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-base">{themeOption.icon}</span>
|
||||||
|
<span className="font-mono">{themeOption.label}</span>
|
||||||
|
{theme === themeOption.value && (
|
||||||
|
<svg className="w-4 h-4 ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { HTMLAttributes, forwardRef, useState, useEffect, useRef } from 'react';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Card } from './Card';
|
import { Card } from './Card';
|
||||||
import { Badge } from './Badge';
|
import { Badge } from './Badge';
|
||||||
|
import { formatDateForDisplay } from '@/lib/date-utils';
|
||||||
|
|
||||||
interface TaskCardProps extends HTMLAttributes<HTMLDivElement> {
|
interface TaskCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
// Variants
|
// Variants
|
||||||
@@ -171,18 +172,16 @@ const TaskCard = forwardRef<HTMLDivElement, TaskCardProps>(
|
|||||||
const getSourceStyles = () => {
|
const getSourceStyles = () => {
|
||||||
if (source === 'jira') {
|
if (source === 'jira') {
|
||||||
return {
|
return {
|
||||||
border: '2px solid rgba(0, 130, 201, 0.8)',
|
backgroundColor: 'var(--jira-card, #dbeafe)',
|
||||||
borderLeft: '6px solid #0052CC',
|
borderLeft: '3px solid var(--jira-border, #3b82f6)',
|
||||||
background: 'linear-gradient(135deg, rgba(0, 130, 201, 0.3) 0%, rgba(0, 130, 201, 0.2) 100%)',
|
color: 'var(--jira-text, #1e40af)',
|
||||||
boxShadow: '0 4px 12px rgba(0, 130, 201, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.3)',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (source === 'tfs') {
|
if (source === 'tfs') {
|
||||||
return {
|
return {
|
||||||
border: '2px solid rgba(255, 165, 0, 0.8)',
|
backgroundColor: 'var(--tfs-card, #fed7aa)',
|
||||||
borderLeft: '6px solid #FF8C00',
|
borderLeft: '3px solid var(--tfs-border, #f59e0b)',
|
||||||
background: 'linear-gradient(135deg, rgba(255, 165, 0, 0.3) 0%, rgba(255, 165, 0, 0.2) 100%)',
|
color: 'var(--tfs-text, #92400e)',
|
||||||
boxShadow: '0 4px 12px rgba(255, 165, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.3)',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
@@ -224,20 +223,17 @@ const TaskCard = forwardRef<HTMLDivElement, TaskCardProps>(
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={sourceStyles}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'hover:border-[var(--primary)]/30 hover:shadow-lg hover:shadow-[var(--primary)]/10 transition-all duration-300 cursor-pointer group',
|
'hover:border-[var(--primary)]/30 hover:shadow-lg hover:shadow-[var(--primary)]/10 transition-all duration-300 cursor-pointer group',
|
||||||
isDragging && 'opacity-50 rotate-3 scale-105',
|
isDragging && 'opacity-50 rotate-3 scale-105',
|
||||||
(status === 'done' || status === 'archived') && 'opacity-60',
|
(status === 'done' || status === 'archived') && 'opacity-60',
|
||||||
status === 'freeze' && 'opacity-60 bg-gradient-to-br from-transparent via-[var(--muted)]/10 to-transparent bg-[length:4px_4px] bg-[linear-gradient(45deg,transparent_25%,var(--border)_25%,var(--border)_50%,transparent_50%,transparent_75%,var(--border)_75%,var(--border))]',
|
status === 'freeze' && 'opacity-60 bg-gradient-to-br from-transparent via-[var(--muted)]/10 to-transparent bg-[length:4px_4px] bg-[linear-gradient(45deg,transparent_25%,var(--border)_25%,var(--border)_50%,transparent_50%,transparent_75%,var(--border)_75%,var(--border))]',
|
||||||
source === 'jira' && 'bg-blue-50 dark:bg-blue-900/20',
|
|
||||||
source === 'tfs' && 'bg-orange-50 dark:bg-orange-900/20',
|
|
||||||
isPending && 'opacity-70 pointer-events-none',
|
isPending && 'opacity-70 pointer-events-none',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="p-2">
|
<div className="p-2" style={sourceStyles}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Emojis */}
|
{/* Emojis */}
|
||||||
{displayEmojis.length > 0 && (
|
{displayEmojis.length > 0 && (
|
||||||
@@ -318,20 +314,17 @@ const TaskCard = forwardRef<HTMLDivElement, TaskCardProps>(
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={sourceStyles}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'hover:border-[var(--primary)]/30 hover:shadow-lg hover:shadow-[var(--primary)]/10 transition-all duration-300 cursor-pointer group',
|
'hover:border-[var(--primary)]/30 hover:shadow-lg hover:shadow-[var(--primary)]/10 transition-all duration-300 cursor-pointer group',
|
||||||
isDragging && 'opacity-50 rotate-3 scale-105',
|
isDragging && 'opacity-50 rotate-3 scale-105',
|
||||||
(status === 'done' || status === 'archived') && 'opacity-60',
|
(status === 'done' || status === 'archived') && 'opacity-60',
|
||||||
status === 'freeze' && 'opacity-60 bg-gradient-to-br from-transparent via-[var(--muted)]/10 to-transparent bg-[length:4px_4px] bg-[linear-gradient(45deg,transparent_25%,var(--border)_25%,var(--border)_50%,transparent_50%,transparent_75%,var(--border)_75%,var(--border))]',
|
status === 'freeze' && 'opacity-60 bg-gradient-to-br from-transparent via-[var(--muted)]/10 to-transparent bg-[length:4px_4px] bg-[linear-gradient(45deg,transparent_25%,var(--border)_25%,var(--border)_50%,transparent_50%,transparent_75%,var(--border)_75%,var(--border))]',
|
||||||
source === 'jira' && 'bg-blue-50 dark:bg-blue-900/20',
|
|
||||||
source === 'tfs' && 'bg-orange-50 dark:bg-orange-900/20',
|
|
||||||
isPending && 'opacity-70 pointer-events-none',
|
isPending && 'opacity-70 pointer-events-none',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={`px-3 pt-3 ${(dueDate || (source && source !== 'manual') || completedAt) ? 'pb-3' : 'pb-0'}`}>
|
<div className={`px-3 pt-3 ${(dueDate || (source && source !== 'manual') || completedAt) ? 'pb-3' : 'pb-0'}`} style={sourceStyles}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-start gap-2 mb-2">
|
<div className="flex items-start gap-2 mb-2">
|
||||||
{/* Emojis */}
|
{/* Emojis */}
|
||||||
@@ -452,7 +445,7 @@ const TaskCard = forwardRef<HTMLDivElement, TaskCardProps>(
|
|||||||
{dueDate ? (
|
{dueDate ? (
|
||||||
<span className="flex items-center gap-1 text-[var(--muted-foreground)] font-mono">
|
<span className="flex items-center gap-1 text-[var(--muted-foreground)] font-mono">
|
||||||
<span className="text-[var(--primary)]">⏰</span>
|
<span className="text-[var(--primary)]">⏰</span>
|
||||||
{dueDate.toLocaleDateString()}
|
{formatDateForDisplay(dueDate, 'DISPLAY_MEDIUM')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|||||||
@@ -24,5 +24,4 @@ export { DropZone } from './DropZone';
|
|||||||
|
|
||||||
// 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 { FontSizeToggle } from './FontSizeToggle';
|
export { FontSizeToggle } from './FontSizeToggle';
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||||
import { updateViewPreferences } from '@/actions/preferences';
|
import { updateViewPreferences } from '@/actions/preferences';
|
||||||
import { Theme } from '@/lib/types';
|
import { Theme } from '@/lib/theme-config';
|
||||||
|
import { THEME_CONFIG, getNextDarkTheme } from '@/lib/theme-config';
|
||||||
|
|
||||||
interface ThemeContextType {
|
interface ThemeContextType {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
toggleTheme: () => void;
|
toggleTheme: () => void;
|
||||||
setTheme: (theme: Theme) => void;
|
setTheme: (theme: Theme) => void;
|
||||||
|
cycleDarkThemes: () => void;
|
||||||
userPreferredTheme: Theme;
|
userPreferredTheme: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTh
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
// Remove all existing theme classes
|
// Remove all existing theme classes
|
||||||
document.documentElement.classList.remove('light', 'dark', 'dracula', 'monokai', 'nord', 'gruvbox', 'tokyo_night', 'catppuccin', 'rose_pine', 'one_dark', 'material', 'solarized');
|
document.documentElement.classList.remove(...THEME_CONFIG.allThemes);
|
||||||
// Add the current theme class
|
// Add the current theme class
|
||||||
document.documentElement.classList.add(theme);
|
document.documentElement.classList.add(theme);
|
||||||
}
|
}
|
||||||
@@ -78,8 +80,20 @@ export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTh
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cycleDarkThemes = async () => {
|
||||||
|
// Si on est sur light, on passe au premier thème dark
|
||||||
|
if (theme === 'light') {
|
||||||
|
await setTheme(THEME_CONFIG.darkThemes[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sinon, on passe au thème dark suivant
|
||||||
|
const nextTheme = getNextDarkTheme(theme);
|
||||||
|
await setTheme(nextTheme);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme, userPreferredTheme }}>
|
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme, cycleDarkThemes, userPreferredTheme }}>
|
||||||
<div className={mounted ? theme : initialTheme}>
|
<div className={mounted ? theme : initialTheme}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
30
src/hooks/useKeyboardShortcuts.ts
Normal file
30
src/hooks/useKeyboardShortcuts.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useTheme } from '@/contexts/ThemeContext';
|
||||||
|
|
||||||
|
export function useKeyboardShortcuts() {
|
||||||
|
const { toggleTheme, cycleDarkThemes } = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
// Cmd + T pour basculer entre light et le thème dark préféré
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'D') {
|
||||||
|
event.preventDefault();
|
||||||
|
toggleTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmd + Shift + T pour faire tourner les thèmes dark
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'T') {
|
||||||
|
event.preventDefault();
|
||||||
|
cycleDarkThemes();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [toggleTheme, cycleDarkThemes]);
|
||||||
|
}
|
||||||
76
src/lib/theme-config.ts
Normal file
76
src/lib/theme-config.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Types de thèmes
|
||||||
|
export type Theme = 'light' | 'dark' | 'dracula' | 'monokai' | 'nord' | 'gruvbox' | 'tokyo_night' | 'catppuccin' | 'rose_pine' | 'one_dark' | 'material' | 'solarized';
|
||||||
|
|
||||||
|
// Configuration des thèmes
|
||||||
|
export const THEME_CONFIG = {
|
||||||
|
// Thème par défaut
|
||||||
|
default: 'dark' as Theme,
|
||||||
|
|
||||||
|
// Thème light
|
||||||
|
light: 'light' as Theme,
|
||||||
|
|
||||||
|
// Liste de tous les thèmes dark disponibles
|
||||||
|
darkThemes: [
|
||||||
|
'dark',
|
||||||
|
'dracula',
|
||||||
|
'monokai',
|
||||||
|
'nord',
|
||||||
|
'gruvbox',
|
||||||
|
'tokyo_night',
|
||||||
|
'catppuccin',
|
||||||
|
'rose_pine',
|
||||||
|
'one_dark',
|
||||||
|
'material',
|
||||||
|
'solarized'
|
||||||
|
] as Theme[],
|
||||||
|
|
||||||
|
// Tous les thèmes disponibles
|
||||||
|
allThemes: [
|
||||||
|
'light',
|
||||||
|
'dark',
|
||||||
|
'dracula',
|
||||||
|
'monokai',
|
||||||
|
'nord',
|
||||||
|
'gruvbox',
|
||||||
|
'tokyo_night',
|
||||||
|
'catppuccin',
|
||||||
|
'rose_pine',
|
||||||
|
'one_dark',
|
||||||
|
'material',
|
||||||
|
'solarized'
|
||||||
|
] as Theme[],
|
||||||
|
|
||||||
|
// Métadonnées des thèmes (pour l'UI future)
|
||||||
|
metadata: {
|
||||||
|
light: { name: 'Light', description: 'Thème clair par défaut', icon: '☀️' },
|
||||||
|
dark: { name: 'Dark', description: 'Thème sombre classique', icon: '🌙' },
|
||||||
|
dracula: { name: 'Dracula', description: 'Inspiré du thème Dracula', icon: '🧛' },
|
||||||
|
monokai: { name: 'Monokai', description: 'Inspiré du thème Monokai', icon: '🎨' },
|
||||||
|
nord: { name: 'Nord', description: 'Palette Nord arctique', icon: '❄️' },
|
||||||
|
gruvbox: { name: 'Gruvbox', description: 'Palette Gruvbox retro', icon: '🎭' },
|
||||||
|
tokyo_night: { name: 'Tokyo Night', description: 'Nuit tokyoïte', icon: '🌃' },
|
||||||
|
catppuccin: { name: 'Catppuccin', description: 'Palette pastel douce', icon: '🐱' },
|
||||||
|
rose_pine: { name: 'Rose Pine', description: 'Palette rose et pin', icon: '🌹' },
|
||||||
|
one_dark: { name: 'One Dark', description: 'Inspiré d\'Atom One Dark', icon: '🌑' },
|
||||||
|
material: { name: 'Material', description: 'Inspiré de Material Design', icon: '📱' },
|
||||||
|
solarized: { name: 'Solarized', description: 'Palette Solarized', icon: '☀️' }
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Fonctions utilitaires
|
||||||
|
export const getNextDarkTheme = (currentTheme: Theme): Theme => {
|
||||||
|
const currentIndex = THEME_CONFIG.darkThemes.indexOf(currentTheme);
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
return THEME_CONFIG.darkThemes[0];
|
||||||
|
}
|
||||||
|
const nextIndex = (currentIndex + 1) % THEME_CONFIG.darkThemes.length;
|
||||||
|
return THEME_CONFIG.darkThemes[nextIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isDarkTheme = (theme: Theme): boolean => {
|
||||||
|
return THEME_CONFIG.darkThemes.includes(theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getThemeMetadata = (theme: Theme) => {
|
||||||
|
return THEME_CONFIG.metadata[theme] || { name: theme, description: 'Thème personnalisé', icon: '🎨' };
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TfsConfig } from '@/services/integrations/tfs';
|
import { TfsConfig } from '@/services/integrations/tfs';
|
||||||
|
import { Theme } from './theme-config';
|
||||||
|
|
||||||
// Types de base pour les tâches
|
// Types de base pour les tâches
|
||||||
// Note: TaskStatus et TaskPriority sont maintenant gérés par la configuration centralisée dans lib/status-config.ts
|
// Note: TaskStatus et TaskPriority sont maintenant gérés par la configuration centralisée dans lib/status-config.ts
|
||||||
@@ -13,8 +14,7 @@ export type TaskStatus =
|
|||||||
export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
|
export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
|
||||||
export type TaskSource = 'reminders' | 'jira' | 'tfs' | 'manual';
|
export type TaskSource = 'reminders' | 'jira' | 'tfs' | 'manual';
|
||||||
|
|
||||||
// Types de thèmes partagés
|
// Types de thèmes partagés - maintenant dans theme-config.ts
|
||||||
export type Theme = 'light' | 'dark' | 'dracula' | 'monokai' | 'nord' | 'gruvbox' | 'tokyo_night' | 'catppuccin' | 'rose_pine' | 'one_dark' | 'material' | 'solarized';
|
|
||||||
|
|
||||||
// Interface centralisée pour les statistiques
|
// Interface centralisée pour les statistiques
|
||||||
export interface TaskStats {
|
export interface TaskStats {
|
||||||
|
|||||||
Reference in New Issue
Block a user