feat: integrate ToastProvider and enhance theme management
- Added `ToastProvider` to `RootLayout` for improved user feedback on theme changes. - Updated `ThemeProvider` to display toast notifications with theme names and icons upon theme changes. - Refactored theme-related imports to streamline code and improve maintainability. - Simplified background cycling logic in `useBackgroundCycle` to utilize centralized background definitions. - Cleaned up unused background definitions in `BackgroundContext` for better clarity and performance.
This commit is contained in:
@@ -9,6 +9,7 @@ import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext";
|
|||||||
import { userPreferencesService } from "@/services/core/user-preferences";
|
import { userPreferencesService } from "@/services/core/user-preferences";
|
||||||
import { KeyboardShortcuts } from "@/components/KeyboardShortcuts";
|
import { KeyboardShortcuts } from "@/components/KeyboardShortcuts";
|
||||||
import { GlobalKeyboardShortcuts } from "@/components/GlobalKeyboardShortcuts";
|
import { GlobalKeyboardShortcuts } from "@/components/GlobalKeyboardShortcuts";
|
||||||
|
import { ToastProvider } from "@/components/ui/Toast";
|
||||||
import { AuthProvider } from "../components/AuthProvider";
|
import { AuthProvider } from "../components/AuthProvider";
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from '@/lib/auth';
|
import { authOptions } from '@/lib/auth';
|
||||||
@@ -48,22 +49,24 @@ export default async function RootLayout({
|
|||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ThemeProvider
|
<ToastProvider>
|
||||||
initialTheme={initialPreferences?.viewPreferences.theme || 'light'}
|
<ThemeProvider
|
||||||
userPreferredTheme={initialPreferences?.viewPreferences.theme === 'light' ? 'dark' : initialPreferences?.viewPreferences.theme || 'light'}
|
initialTheme={initialPreferences?.viewPreferences.theme || 'light'}
|
||||||
>
|
userPreferredTheme={initialPreferences?.viewPreferences.theme === 'light' ? 'dark' : initialPreferences?.viewPreferences.theme || 'light'}
|
||||||
<KeyboardShortcutsProvider>
|
>
|
||||||
<KeyboardShortcuts />
|
<KeyboardShortcutsProvider>
|
||||||
<JiraConfigProvider config={initialPreferences?.jiraConfig || { enabled: false }}>
|
<KeyboardShortcuts />
|
||||||
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
<JiraConfigProvider config={initialPreferences?.jiraConfig || { enabled: false }}>
|
||||||
<GlobalKeyboardShortcuts />
|
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
||||||
<BackgroundProvider>
|
<GlobalKeyboardShortcuts />
|
||||||
{children}
|
<BackgroundProvider>
|
||||||
</BackgroundProvider>
|
{children}
|
||||||
</UserPreferencesProvider>
|
</BackgroundProvider>
|
||||||
</JiraConfigProvider>
|
</UserPreferencesProvider>
|
||||||
</KeyboardShortcutsProvider>
|
</JiraConfigProvider>
|
||||||
</ThemeProvider>
|
</KeyboardShortcutsProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</ToastProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme } from '@/contexts/ThemeContext';
|
||||||
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';
|
import { Theme, THEME_CONFIG, THEME_NAMES, getThemeIcon, getThemeDescription } from '@/lib/ui-config';
|
||||||
|
|
||||||
|
|
||||||
// Génération des thèmes à partir de la configuration centralisée
|
// Génération des thèmes à partir de la configuration centralisée
|
||||||
const themes: { id: Theme; name: string; description: string }[] = THEME_CONFIG.allThemes.map(themeId => {
|
const themes: { id: Theme; name: string; description: string; icon: string }[] = THEME_CONFIG.allThemes.map(themeId => {
|
||||||
const metadata = getThemeMetadata(themeId);
|
|
||||||
return {
|
return {
|
||||||
id: themeId,
|
id: themeId,
|
||||||
name: metadata.name,
|
name: THEME_NAMES[themeId],
|
||||||
description: metadata.description
|
description: getThemeDescription(themeId),
|
||||||
|
icon: getThemeIcon(themeId)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,7 +94,8 @@ export function ThemeSelector() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-medium text-[var(--foreground)] mb-1">
|
<div className="font-medium text-[var(--foreground)] mb-1 flex items-center gap-2">
|
||||||
|
<span className="text-base">{themeOption.icon}</span>
|
||||||
{themeOption.name}
|
{themeOption.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-[var(--muted-foreground)] leading-relaxed">
|
<div className="text-xs text-[var(--muted-foreground)] leading-relaxed">
|
||||||
|
|||||||
@@ -4,83 +4,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Card, CardContent } from '@/components/ui/Card';
|
import { Card, CardContent } from '@/components/ui/Card';
|
||||||
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
|
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
|
||||||
|
import { PRESET_BACKGROUNDS } from '@/lib/ui-config';
|
||||||
// Images de fond prédéfinies basées sur le thème actuel
|
|
||||||
const PRESET_BACKGROUNDS = [
|
|
||||||
{
|
|
||||||
id: 'none',
|
|
||||||
name: 'Aucune',
|
|
||||||
description: 'Fond uni par défaut',
|
|
||||||
preview: 'linear-gradient(135deg, var(--background) 0%, var(--card) 100%)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'theme-subtle',
|
|
||||||
name: 'Dégradé subtil',
|
|
||||||
description: 'Dégradé doux avec les couleurs du thème',
|
|
||||||
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--primary) 15%, var(--background)) 100%)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'theme-primary',
|
|
||||||
name: 'Dégradé primaire',
|
|
||||||
description: 'Dégradé marqué avec la couleur primaire',
|
|
||||||
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--primary) 25%, var(--background)) 100%)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'theme-accent',
|
|
||||||
name: 'Dégradé accent',
|
|
||||||
description: 'Dégradé marqué avec la couleur d\'accent',
|
|
||||||
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--accent) 25%, var(--background)) 100%)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'theme-success',
|
|
||||||
name: 'Dégradé succès',
|
|
||||||
description: 'Dégradé marqué avec la couleur de succès',
|
|
||||||
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--success) 25%, var(--background)) 100%)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'theme-purple',
|
|
||||||
name: 'Dégradé violet',
|
|
||||||
description: 'Dégradé marqué avec la couleur violette',
|
|
||||||
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--purple) 25%, var(--background)) 100%)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'theme-diagonal',
|
|
||||||
name: 'Dégradé diagonal',
|
|
||||||
description: 'Dégradé diagonal avec plusieurs couleurs',
|
|
||||||
preview: 'linear-gradient(45deg, var(--background) 0%, color-mix(in srgb, var(--primary) 20%, var(--background)) 50%, color-mix(in srgb, var(--accent) 20%, var(--background)) 100%)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'theme-radial',
|
|
||||||
name: 'Dégradé radial',
|
|
||||||
description: 'Dégradé radial centré avec les couleurs du thème',
|
|
||||||
preview: 'radial-gradient(circle at center, var(--background) 0%, color-mix(in srgb, var(--primary) 20%, var(--background)) 100%)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'theme-sunset',
|
|
||||||
name: 'Dégradé coucher de soleil',
|
|
||||||
description: 'Dégradé orange-rouge intense',
|
|
||||||
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--accent) 30%, var(--background)) 0%, color-mix(in srgb, var(--destructive) 20%, var(--background)) 100%)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'theme-ocean',
|
|
||||||
name: 'Dégradé océan',
|
|
||||||
description: 'Dégradé bleu profond',
|
|
||||||
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--blue) 25%, var(--background)) 0%, color-mix(in srgb, var(--primary) 15%, var(--background)) 100%)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'theme-forest',
|
|
||||||
name: 'Dégradé forêt',
|
|
||||||
description: 'Dégradé vert naturel',
|
|
||||||
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--green) 25%, var(--background)) 0%, color-mix(in srgb, var(--success) 15%, var(--background)) 100%)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'theme-galaxy',
|
|
||||||
name: 'Dégradé galaxie',
|
|
||||||
description: 'Dégradé violet-bleu mystique',
|
|
||||||
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--purple) 25%, var(--background)) 0%, color-mix(in srgb, var(--blue) 20%, var(--background)) 100%)'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
export function BackgroundImageSelector() {
|
export function BackgroundImageSelector() {
|
||||||
const { preferences, updateViewPreferences } = useUserPreferences();
|
const { preferences, updateViewPreferences } = useUserPreferences();
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ 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, THEME_CONFIG, getThemeIcon } from '@/lib/ui-config';
|
||||||
import { THEME_CONFIG, getThemeMetadata } from '@/lib/theme-config';
|
|
||||||
import { useKeyboardShortcutsModal } from '@/contexts/KeyboardShortcutsContext';
|
import { useKeyboardShortcutsModal } from '@/contexts/KeyboardShortcutsContext';
|
||||||
import { AuthButton } from '@/components/AuthButton';
|
import { AuthButton } from '@/components/AuthButton';
|
||||||
import { useSession, signOut } from 'next-auth/react';
|
import { useSession, signOut } from 'next-auth/react';
|
||||||
@@ -28,12 +27,10 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
|||||||
|
|
||||||
// Liste des thèmes disponibles avec leurs labels et icônes
|
// 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 themes: { value: Theme; label: string; icon: string }[] = THEME_CONFIG.allThemes.map(themeValue => {
|
||||||
const metadata = getThemeMetadata(themeValue);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: themeValue,
|
value: themeValue,
|
||||||
label: metadata.name,
|
label: themeValue.charAt(0).toUpperCase() + themeValue.slice(1).replace('_', ' '),
|
||||||
icon: metadata.icon
|
icon: getThemeIcon(themeValue)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
94
src/components/ui/Toast.tsx
Normal file
94
src/components/ui/Toast.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
interface ToastProps {
|
||||||
|
message: string;
|
||||||
|
duration?: number;
|
||||||
|
onClose: () => void;
|
||||||
|
index: number;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Toast({ message, duration = 2000, onClose, index, icon }: ToastProps) {
|
||||||
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setTimeout(onClose, 200); // Attendre la fin de l'animation
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [duration, onClose]);
|
||||||
|
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className={`fixed top-4 right-4 z-50 transition-all duration-200 ${
|
||||||
|
isVisible
|
||||||
|
? 'opacity-100 translate-y-0'
|
||||||
|
: 'opacity-0 -translate-y-2'
|
||||||
|
}`}
|
||||||
|
style={{ transform: `translateY(${index * 60}px)` }}
|
||||||
|
>
|
||||||
|
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg px-3 py-2 shadow-lg backdrop-blur-sm flex items-center gap-2">
|
||||||
|
{icon && <span className="text-base">{icon}</span>}
|
||||||
|
<div className="text-sm text-[var(--foreground)] font-medium">
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToastContextType {
|
||||||
|
showToast: (message: string, duration?: number, icon?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToastContext = React.createContext<ToastContextType | null>(null);
|
||||||
|
|
||||||
|
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [toasts, setToasts] = useState<Array<{ id: string; message: string; duration?: number; icon?: string }>>([]);
|
||||||
|
const MAX_TOASTS = 5;
|
||||||
|
|
||||||
|
const showToast = (message: string, duration?: number, icon?: string) => {
|
||||||
|
const id = Math.random().toString(36).substr(2, 9);
|
||||||
|
setToasts(prev => {
|
||||||
|
const newToasts = [...prev, { id, message, duration, icon }];
|
||||||
|
// Garder seulement les 5 plus récents
|
||||||
|
return newToasts.slice(-MAX_TOASTS);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeToast = (id: string) => {
|
||||||
|
setToasts(prev => prev.filter(toast => toast.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={{ showToast }}>
|
||||||
|
{children}
|
||||||
|
{toasts.map((toast, index) => (
|
||||||
|
<Toast
|
||||||
|
key={toast.id}
|
||||||
|
message={toast.message}
|
||||||
|
duration={toast.duration}
|
||||||
|
onClose={() => removeToast(toast.id)}
|
||||||
|
index={index}
|
||||||
|
icon={toast.icon}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToast() {
|
||||||
|
const context = React.useContext(ToastContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useToast must be used within a ToastProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||||
import { useUserPreferences } from './UserPreferencesContext';
|
import { useUserPreferences } from './UserPreferencesContext';
|
||||||
|
import { PRESET_BACKGROUNDS } from '@/lib/ui-config';
|
||||||
|
|
||||||
interface BackgroundContextType {
|
interface BackgroundContextType {
|
||||||
backgroundImage: string | undefined;
|
backgroundImage: string | undefined;
|
||||||
@@ -69,37 +70,14 @@ export function BackgroundProvider({ children }: BackgroundProviderProps) {
|
|||||||
backgroundElement.style.pointerEvents = 'none';
|
backgroundElement.style.pointerEvents = 'none';
|
||||||
|
|
||||||
// Vérifier si c'est une URL d'image ou un preset
|
// Vérifier si c'est une URL d'image ou un preset
|
||||||
const PRESET_BACKGROUNDS = [
|
const presetIds = PRESET_BACKGROUNDS.map(preset => preset.id);
|
||||||
'theme-subtle',
|
|
||||||
'theme-primary',
|
if (backgroundImage && presetIds.includes(backgroundImage as typeof presetIds[number])) {
|
||||||
'theme-accent',
|
// Trouver le preset correspondant
|
||||||
'theme-success',
|
const preset = PRESET_BACKGROUNDS.find(p => p.id === backgroundImage);
|
||||||
'theme-purple',
|
if (preset) {
|
||||||
'theme-diagonal',
|
backgroundElement.style.backgroundImage = preset.preview;
|
||||||
'theme-radial',
|
}
|
||||||
'theme-sunset',
|
|
||||||
'theme-ocean',
|
|
||||||
'theme-forest',
|
|
||||||
'theme-galaxy'
|
|
||||||
];
|
|
||||||
|
|
||||||
if (PRESET_BACKGROUNDS.includes(backgroundImage)) {
|
|
||||||
// Appliquer le preset basé sur le thème
|
|
||||||
const presetStyles = {
|
|
||||||
'theme-subtle': 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--primary) 15%, var(--background)) 100%)',
|
|
||||||
'theme-primary': 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--primary) 25%, var(--background)) 100%)',
|
|
||||||
'theme-accent': 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--accent) 25%, var(--background)) 100%)',
|
|
||||||
'theme-success': 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--success) 25%, var(--background)) 100%)',
|
|
||||||
'theme-purple': 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--purple) 25%, var(--background)) 100%)',
|
|
||||||
'theme-diagonal': 'linear-gradient(45deg, var(--background) 0%, color-mix(in srgb, var(--primary) 20%, var(--background)) 50%, color-mix(in srgb, var(--accent) 20%, var(--background)) 100%)',
|
|
||||||
'theme-radial': 'radial-gradient(circle at center, var(--background) 0%, color-mix(in srgb, var(--primary) 20%, var(--background)) 100%)',
|
|
||||||
'theme-sunset': 'linear-gradient(135deg, color-mix(in srgb, var(--accent) 30%, var(--background)) 0%, color-mix(in srgb, var(--destructive) 20%, var(--background)) 100%)',
|
|
||||||
'theme-ocean': 'linear-gradient(135deg, color-mix(in srgb, var(--blue) 25%, var(--background)) 0%, color-mix(in srgb, var(--primary) 15%, var(--background)) 100%)',
|
|
||||||
'theme-forest': 'linear-gradient(135deg, color-mix(in srgb, var(--green) 25%, var(--background)) 0%, color-mix(in srgb, var(--success) 15%, var(--background)) 100%)',
|
|
||||||
'theme-galaxy': 'linear-gradient(135deg, color-mix(in srgb, var(--purple) 25%, var(--background)) 0%, color-mix(in srgb, var(--blue) 20%, var(--background)) 100%)'
|
|
||||||
};
|
|
||||||
|
|
||||||
backgroundElement.style.backgroundImage = presetStyles[backgroundImage as keyof typeof presetStyles];
|
|
||||||
} else {
|
} else {
|
||||||
// Appliquer l'URL d'image personnalisée
|
// Appliquer l'URL d'image personnalisée
|
||||||
backgroundElement.style.backgroundImage = `url(${backgroundImage})`;
|
backgroundElement.style.backgroundImage = `url(${backgroundImage})`;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
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/theme-config';
|
import { useToast } from '@/components/ui/Toast';
|
||||||
import { THEME_CONFIG, getNextDarkTheme } from '@/lib/theme-config';
|
import { Theme, THEME_CONFIG, getNextDarkTheme, THEME_NAMES, getThemeIcon } from '@/lib/ui-config';
|
||||||
|
|
||||||
interface ThemeContextType {
|
interface ThemeContextType {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
@@ -25,6 +25,7 @@ export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTh
|
|||||||
const [theme, setThemeState] = useState<Theme>(initialTheme);
|
const [theme, setThemeState] = useState<Theme>(initialTheme);
|
||||||
const [userPreferredTheme, setUserPreferredTheme] = useState<Theme>(initialUserPreferredTheme);
|
const [userPreferredTheme, setUserPreferredTheme] = useState<Theme>(initialUserPreferredTheme);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
// Hydration safe initialization
|
// Hydration safe initialization
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -57,6 +58,9 @@ export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTh
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la sauvegarde du thème:', error);
|
console.error('Erreur lors de la sauvegarde du thème:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Afficher le toast avec le nom du thème
|
||||||
|
showToast(`Thème: ${THEME_NAMES[newTheme]}`, 2000, getThemeIcon(newTheme));
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTheme = async (newTheme: Theme) => {
|
const setTheme = async (newTheme: Theme) => {
|
||||||
@@ -78,6 +82,9 @@ export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTh
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la sauvegarde du thème:', error);
|
console.error('Erreur lors de la sauvegarde du thème:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Afficher le toast avec le nom du thème
|
||||||
|
showToast(`Thème: ${THEME_NAMES[newTheme]}`, 2000, getThemeIcon(newTheme));
|
||||||
};
|
};
|
||||||
|
|
||||||
const cycleDarkThemes = async () => {
|
const cycleDarkThemes = async () => {
|
||||||
|
|||||||
@@ -1,66 +1,25 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
|
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
|
||||||
|
import { useToast } from '@/components/ui/Toast';
|
||||||
// Liste des backgrounds prédéfinis pour le cycle
|
import { BACKGROUND_NAMES, TOAST_ICONS, getNextBackground } from '@/lib/ui-config';
|
||||||
const BACKGROUND_CYCLE = [
|
|
||||||
'none',
|
|
||||||
'theme-subtle',
|
|
||||||
'theme-primary',
|
|
||||||
'theme-accent',
|
|
||||||
'theme-success',
|
|
||||||
'theme-purple',
|
|
||||||
'theme-diagonal',
|
|
||||||
'theme-radial',
|
|
||||||
'theme-sunset',
|
|
||||||
'theme-ocean',
|
|
||||||
'theme-forest',
|
|
||||||
'theme-galaxy'
|
|
||||||
];
|
|
||||||
|
|
||||||
export function useBackgroundCycle() {
|
export function useBackgroundCycle() {
|
||||||
const { preferences, updateViewPreferences } = useUserPreferences();
|
const { preferences, updateViewPreferences } = useUserPreferences();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
const cycleBackground = () => {
|
const cycleBackground = () => {
|
||||||
const currentBackground = preferences?.viewPreferences?.backgroundImage;
|
const currentBackground = preferences?.viewPreferences?.backgroundImage;
|
||||||
const customImages = preferences?.viewPreferences?.customImages || [];
|
const customImages = preferences?.viewPreferences?.customImages || [];
|
||||||
|
|
||||||
// Construire la liste complète des backgrounds (prédéfinis + personnalisés)
|
const nextBackground = getNextBackground(currentBackground || 'none', customImages);
|
||||||
const allBackgrounds = [...BACKGROUND_CYCLE];
|
|
||||||
|
|
||||||
// Ajouter toutes les images personnalisées
|
|
||||||
customImages.forEach(url => {
|
|
||||||
if (!allBackgrounds.includes(url)) {
|
|
||||||
allBackgrounds.push(url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentIndex = allBackgrounds.findIndex(bg => bg === currentBackground);
|
|
||||||
|
|
||||||
// Si on ne trouve pas l'index, c'est qu'on est sur "none" (undefined)
|
|
||||||
// Dans ce cas, on commence au début de la liste
|
|
||||||
let actualCurrentIndex = currentIndex;
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
actualCurrentIndex = -1; // On est sur "none", on va commencer à l'index 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextIndex = (actualCurrentIndex + 1) % allBackgrounds.length;
|
|
||||||
const nextBackground = allBackgrounds[nextIndex];
|
|
||||||
|
|
||||||
// Si c'est 'none', on met undefined
|
|
||||||
const backgroundImage = nextBackground === 'none' ? undefined : nextBackground;
|
const backgroundImage = nextBackground === 'none' ? undefined : nextBackground;
|
||||||
|
|
||||||
// Si on est sur "none" (undefined) et qu'on va vers "none", on va vers le suivant
|
|
||||||
if (currentBackground === undefined && nextBackground === 'none') {
|
|
||||||
const nextNextIndex = (nextIndex + 1) % allBackgrounds.length;
|
|
||||||
const nextNextBackground = allBackgrounds[nextNextIndex];
|
|
||||||
const finalBackgroundImage = nextNextBackground === 'none' ? undefined : nextNextBackground;
|
|
||||||
|
|
||||||
updateViewPreferences({ backgroundImage: finalBackgroundImage });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateViewPreferences({ backgroundImage });
|
updateViewPreferences({ backgroundImage });
|
||||||
|
|
||||||
|
// Afficher le toast avec le nom du background
|
||||||
|
const backgroundName = BACKGROUND_NAMES[nextBackground] || 'Image personnalisée';
|
||||||
|
showToast(`Background: ${backgroundName}`, 2000, TOAST_ICONS.background);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const THEME_CONFIG = {
|
|||||||
'solarized'
|
'solarized'
|
||||||
] as Theme[],
|
] as Theme[],
|
||||||
|
|
||||||
// Métadonnées des thèmes (pour l'UI future)
|
// Métadonnées des thèmes (déplacées vers ui-config.ts)
|
||||||
metadata: {
|
metadata: {
|
||||||
light: { name: 'Light', description: 'Thème clair par défaut', icon: '☀️' },
|
light: { name: 'Light', description: 'Thème clair par défaut', icon: '☀️' },
|
||||||
dark: { name: 'Dark', description: 'Thème sombre classique', icon: '🌙' },
|
dark: { name: 'Dark', description: 'Thème sombre classique', icon: '🌙' },
|
||||||
|
|||||||
254
src/lib/ui-config.ts
Normal file
254
src/lib/ui-config.ts
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
// ===== CONFIGURATION DES THÈMES =====
|
||||||
|
|
||||||
|
// 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[]
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Métadonnées des thèmes pour l'affichage
|
||||||
|
export const THEME_METADATA: Record<Theme, { name: string; description: string; icon: string }> = {
|
||||||
|
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: '☀️' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fonctions utilitaires pour les thèmes
|
||||||
|
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_METADATA[theme] || { name: theme, description: 'Thème personnalisé', icon: '🎨' };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== CONFIGURATION DES BACKGROUNDS =====
|
||||||
|
|
||||||
|
// Configuration des backgrounds prédéfinis
|
||||||
|
export const PRESET_BACKGROUNDS = [
|
||||||
|
{
|
||||||
|
id: 'none',
|
||||||
|
name: 'Aucun fond',
|
||||||
|
description: 'Fond par défaut du système',
|
||||||
|
preview: 'var(--background)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-subtle',
|
||||||
|
name: 'Dégradé subtil',
|
||||||
|
description: 'Dégradé très léger et discret',
|
||||||
|
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--primary) 8%, var(--background)) 0%, color-mix(in srgb, var(--accent) 5%, var(--background)) 100%)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-primary',
|
||||||
|
name: 'Dégradé primaire',
|
||||||
|
description: 'Dégradé avec la couleur primaire',
|
||||||
|
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--primary) 25%, var(--background)) 0%, color-mix(in srgb, var(--primary) 15%, var(--background)) 100%)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-accent',
|
||||||
|
name: 'Dégradé accent',
|
||||||
|
description: 'Dégradé avec la couleur accent',
|
||||||
|
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--accent) 25%, var(--background)) 0%, color-mix(in srgb, var(--accent) 15%, var(--background)) 100%)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-success',
|
||||||
|
name: 'Dégradé vert',
|
||||||
|
description: 'Dégradé avec la couleur de succès',
|
||||||
|
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--success) 25%, var(--background)) 0%, color-mix(in srgb, var(--success) 15%, var(--background)) 100%)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-purple',
|
||||||
|
name: 'Dégradé violet',
|
||||||
|
description: 'Dégradé avec la couleur violette',
|
||||||
|
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--purple) 25%, var(--background)) 0%, color-mix(in srgb, var(--purple) 15%, var(--background)) 100%)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-diagonal',
|
||||||
|
name: 'Dégradé diagonal',
|
||||||
|
description: 'Dégradé diagonal dynamique',
|
||||||
|
preview: 'linear-gradient(45deg, color-mix(in srgb, var(--primary) 20%, var(--background)) 0%, color-mix(in srgb, var(--accent) 20%, var(--background)) 50%, color-mix(in srgb, var(--success) 20%, var(--background)) 100%)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-radial',
|
||||||
|
name: 'Dégradé radial',
|
||||||
|
description: 'Dégradé radial depuis le centre',
|
||||||
|
preview: 'radial-gradient(circle, color-mix(in srgb, var(--primary) 20%, var(--background)) 0%, color-mix(in srgb, var(--accent) 10%, var(--background)) 70%, var(--background) 100%)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-sunset',
|
||||||
|
name: 'Dégradé coucher',
|
||||||
|
description: 'Dégradé inspiré du coucher de soleil',
|
||||||
|
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--accent) 25%, var(--background)) 0%, color-mix(in srgb, var(--purple) 20%, var(--background)) 50%, color-mix(in srgb, var(--primary) 15%, var(--background)) 100%)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-ocean',
|
||||||
|
name: 'Dégradé océan',
|
||||||
|
description: 'Dégradé inspiré de l\'océan',
|
||||||
|
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--primary) 25%, var(--background)) 0%, color-mix(in srgb, var(--blue) 20%, var(--background)) 50%, color-mix(in srgb, var(--success) 15%, var(--background)) 100%)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-forest',
|
||||||
|
name: 'Dégradé forêt',
|
||||||
|
description: 'Dégradé inspiré de la forêt',
|
||||||
|
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--success) 25%, var(--background)) 0%, color-mix(in srgb, var(--green) 20%, var(--background)) 50%, color-mix(in srgb, var(--accent) 15%, var(--background)) 100%)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-galaxy',
|
||||||
|
name: 'Dégradé galaxie',
|
||||||
|
description: 'Dégradé inspiré de la galaxie',
|
||||||
|
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--purple) 25%, var(--background)) 0%, color-mix(in srgb, var(--blue) 20%, var(--background)) 100%)'
|
||||||
|
}
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Liste des backgrounds pour le cycle (IDs seulement)
|
||||||
|
export const BACKGROUND_CYCLE = PRESET_BACKGROUNDS.map(bg => bg.id);
|
||||||
|
|
||||||
|
// Types pour les backgrounds
|
||||||
|
export type BackgroundId = typeof PRESET_BACKGROUNDS[number]['id'];
|
||||||
|
|
||||||
|
// Fonctions utilitaires pour les backgrounds
|
||||||
|
export const getBackgroundById = (id: BackgroundId) => {
|
||||||
|
return PRESET_BACKGROUNDS.find(bg => bg.id === id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isPresetBackground = (id: string): id is BackgroundId => {
|
||||||
|
return PRESET_BACKGROUNDS.some(bg => bg.id === id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNextBackground = (currentBackground: string, customImages: string[] = []): string => {
|
||||||
|
const allBackgrounds = [...BACKGROUND_CYCLE];
|
||||||
|
|
||||||
|
// Ajouter toutes les images personnalisées
|
||||||
|
customImages.forEach(url => {
|
||||||
|
if (!allBackgrounds.includes(url as typeof BACKGROUND_CYCLE[number])) {
|
||||||
|
allBackgrounds.push(url as typeof BACKGROUND_CYCLE[number]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentIndex = allBackgrounds.findIndex(bg => bg === currentBackground);
|
||||||
|
|
||||||
|
// Si on ne trouve pas l'index, c'est qu'on est sur "none" (undefined)
|
||||||
|
let actualCurrentIndex = currentIndex;
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
actualCurrentIndex = -1; // On est sur "none", on va commencer à l'index 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextIndex = (actualCurrentIndex + 1) % allBackgrounds.length;
|
||||||
|
const nextBackground = allBackgrounds[nextIndex];
|
||||||
|
|
||||||
|
// Si on est sur "none" (undefined) et qu'on va vers "none", on va vers le suivant
|
||||||
|
if (currentBackground === undefined && nextBackground === 'none') {
|
||||||
|
const nextNextIndex = (nextIndex + 1) % allBackgrounds.length;
|
||||||
|
return allBackgrounds[nextNextIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextBackground;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== MÉTADONNÉES D'AFFICHAGE =====
|
||||||
|
|
||||||
|
// Mapping des noms de backgrounds pour l'affichage
|
||||||
|
export const BACKGROUND_NAMES: Record<string, string> = {
|
||||||
|
'none': 'Aucun fond',
|
||||||
|
'theme-subtle': 'Dégradé subtil',
|
||||||
|
'theme-primary': 'Dégradé primaire',
|
||||||
|
'theme-accent': 'Dégradé accent',
|
||||||
|
'theme-success': 'Dégradé vert',
|
||||||
|
'theme-purple': 'Dégradé violet',
|
||||||
|
'theme-diagonal': 'Dégradé diagonal',
|
||||||
|
'theme-radial': 'Dégradé radial',
|
||||||
|
'theme-sunset': 'Dégradé coucher',
|
||||||
|
'theme-ocean': 'Dégradé océan',
|
||||||
|
'theme-forest': 'Dégradé forêt',
|
||||||
|
'theme-galaxy': 'Dégradé galaxie'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mapping des noms de thèmes pour l'affichage
|
||||||
|
export const THEME_NAMES: Record<Theme, string> = {
|
||||||
|
'light': 'Thème clair',
|
||||||
|
'dark': 'Thème sombre',
|
||||||
|
'dracula': 'Thème Dracula',
|
||||||
|
'monokai': 'Thème Monokai',
|
||||||
|
'nord': 'Thème Nord',
|
||||||
|
'gruvbox': 'Thème Gruvbox',
|
||||||
|
'tokyo_night': 'Thème Tokyo Night',
|
||||||
|
'catppuccin': 'Thème Catppuccin',
|
||||||
|
'rose_pine': 'Thème Rose Pine',
|
||||||
|
'one_dark': 'Thème One Dark',
|
||||||
|
'material': 'Thème Material',
|
||||||
|
'solarized': 'Thème Solarized'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fonctions utilitaires pour les métadonnées
|
||||||
|
export const getThemeIcon = (theme: Theme): string => {
|
||||||
|
return THEME_METADATA[theme].icon;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getThemeName = (theme: Theme): string => {
|
||||||
|
return THEME_METADATA[theme].name;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getThemeDescription = (theme: Theme): string => {
|
||||||
|
return THEME_METADATA[theme].description;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Icônes pour les toasts
|
||||||
|
export const TOAST_ICONS = {
|
||||||
|
background: '🎨',
|
||||||
|
theme: '🎭'
|
||||||
|
} as const;
|
||||||
Reference in New Issue
Block a user