From 10c1f811cef8499ff16016ffb20bc48ad7fcd13c Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 2 Oct 2025 17:24:37 +0200 Subject: [PATCH] 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. --- src/app/layout.tsx | 35 +-- src/components/ThemeSelector.tsx | 14 +- .../settings/BackgroundImageSelector.tsx | 78 +----- src/components/ui/Header.tsx | 9 +- src/components/ui/Toast.tsx | 94 +++++++ src/contexts/BackgroundContext.tsx | 40 +-- src/contexts/ThemeContext.tsx | 11 +- src/hooks/useBackgroundCycle.ts | 57 +--- src/lib/theme-config.ts | 2 +- src/lib/ui-config.ts | 254 ++++++++++++++++++ 10 files changed, 405 insertions(+), 189 deletions(-) create mode 100644 src/components/ui/Toast.tsx create mode 100644 src/lib/ui-config.ts diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 38dd45b..a976092 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -9,6 +9,7 @@ import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext"; import { userPreferencesService } from "@/services/core/user-preferences"; import { KeyboardShortcuts } from "@/components/KeyboardShortcuts"; import { GlobalKeyboardShortcuts } from "@/components/GlobalKeyboardShortcuts"; +import { ToastProvider } from "@/components/ui/Toast"; import { AuthProvider } from "../components/AuthProvider"; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; @@ -48,22 +49,24 @@ export default async function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > - - - - - - - - {children} - - - - - + + + + + + + + + {children} + + + + + + diff --git a/src/components/ThemeSelector.tsx b/src/components/ThemeSelector.tsx index e312325..592bacd 100644 --- a/src/components/ThemeSelector.tsx +++ b/src/components/ThemeSelector.tsx @@ -1,18 +1,17 @@ 'use client'; import { useTheme } from '@/contexts/ThemeContext'; -import { Theme } from '@/lib/theme-config'; 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 -const themes: { id: Theme; name: string; description: string }[] = THEME_CONFIG.allThemes.map(themeId => { - const metadata = getThemeMetadata(themeId); +const themes: { id: Theme; name: string; description: string; icon: string }[] = THEME_CONFIG.allThemes.map(themeId => { return { id: themeId, - name: metadata.name, - description: metadata.description + name: THEME_NAMES[themeId], + description: getThemeDescription(themeId), + icon: getThemeIcon(themeId) }; }); @@ -95,7 +94,8 @@ export function ThemeSelector() {
-
+
+ {themeOption.icon} {themeOption.name}
diff --git a/src/components/settings/BackgroundImageSelector.tsx b/src/components/settings/BackgroundImageSelector.tsx index ef3aaa2..0384fe0 100644 --- a/src/components/settings/BackgroundImageSelector.tsx +++ b/src/components/settings/BackgroundImageSelector.tsx @@ -4,83 +4,7 @@ import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/Button'; import { Card, CardContent } from '@/components/ui/Card'; import { useUserPreferences } from '@/contexts/UserPreferencesContext'; - -// 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%)' - } -]; - +import { PRESET_BACKGROUNDS } from '@/lib/ui-config'; export function BackgroundImageSelector() { const { preferences, updateViewPreferences } = useUserPreferences(); diff --git a/src/components/ui/Header.tsx b/src/components/ui/Header.tsx index efeb840..10630b4 100644 --- a/src/components/ui/Header.tsx +++ b/src/components/ui/Header.tsx @@ -5,8 +5,7 @@ import { useJiraConfig } from '@/contexts/JiraConfigContext'; import { usePathname } from 'next/navigation'; import Link from 'next/link'; import { useState } from 'react'; -import { Theme } from '@/lib/theme-config'; -import { THEME_CONFIG, getThemeMetadata } from '@/lib/theme-config'; +import { Theme, THEME_CONFIG, getThemeIcon } from '@/lib/ui-config'; import { useKeyboardShortcutsModal } from '@/contexts/KeyboardShortcutsContext'; import { AuthButton } from '@/components/AuthButton'; 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 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 + label: themeValue.charAt(0).toUpperCase() + themeValue.slice(1).replace('_', ' '), + icon: getThemeIcon(themeValue) }; }); diff --git a/src/components/ui/Toast.tsx b/src/components/ui/Toast.tsx new file mode 100644 index 0000000..8a29120 --- /dev/null +++ b/src/components/ui/Toast.tsx @@ -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( +
+
+ {icon && {icon}} +
+ {message} +
+
+
, + document.body + ); +} + +interface ToastContextType { + showToast: (message: string, duration?: number, icon?: string) => void; +} + +export const ToastContext = React.createContext(null); + +export function ToastProvider({ children }: { children: React.ReactNode }) { + const [toasts, setToasts] = useState>([]); + 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 ( + + {children} + {toasts.map((toast, index) => ( + removeToast(toast.id)} + index={index} + icon={toast.icon} + /> + ))} + + ); +} + +export function useToast() { + const context = React.useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within a ToastProvider'); + } + return context; +} diff --git a/src/contexts/BackgroundContext.tsx b/src/contexts/BackgroundContext.tsx index 508d2da..b7fee1d 100644 --- a/src/contexts/BackgroundContext.tsx +++ b/src/contexts/BackgroundContext.tsx @@ -2,6 +2,7 @@ import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { useUserPreferences } from './UserPreferencesContext'; +import { PRESET_BACKGROUNDS } from '@/lib/ui-config'; interface BackgroundContextType { backgroundImage: string | undefined; @@ -69,37 +70,14 @@ export function BackgroundProvider({ children }: BackgroundProviderProps) { backgroundElement.style.pointerEvents = 'none'; // Vérifier si c'est une URL d'image ou un preset - const PRESET_BACKGROUNDS = [ - 'theme-subtle', - 'theme-primary', - 'theme-accent', - 'theme-success', - 'theme-purple', - 'theme-diagonal', - '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]; + const presetIds = PRESET_BACKGROUNDS.map(preset => preset.id); + + if (backgroundImage && presetIds.includes(backgroundImage as typeof presetIds[number])) { + // Trouver le preset correspondant + const preset = PRESET_BACKGROUNDS.find(p => p.id === backgroundImage); + if (preset) { + backgroundElement.style.backgroundImage = preset.preview; + } } else { // Appliquer l'URL d'image personnalisée backgroundElement.style.backgroundImage = `url(${backgroundImage})`; diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index 58d2994..dd41980 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -2,8 +2,8 @@ import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { updateViewPreferences } from '@/actions/preferences'; -import { Theme } from '@/lib/theme-config'; -import { THEME_CONFIG, getNextDarkTheme } from '@/lib/theme-config'; +import { useToast } from '@/components/ui/Toast'; +import { Theme, THEME_CONFIG, getNextDarkTheme, THEME_NAMES, getThemeIcon } from '@/lib/ui-config'; interface ThemeContextType { theme: Theme; @@ -25,6 +25,7 @@ export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTh const [theme, setThemeState] = useState(initialTheme); const [userPreferredTheme, setUserPreferredTheme] = useState(initialUserPreferredTheme); const [mounted, setMounted] = useState(false); + const { showToast } = useToast(); // Hydration safe initialization useEffect(() => { @@ -57,6 +58,9 @@ export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTh } catch (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) => { @@ -78,6 +82,9 @@ export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTh } catch (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 () => { diff --git a/src/hooks/useBackgroundCycle.ts b/src/hooks/useBackgroundCycle.ts index 80af728..75fd90e 100644 --- a/src/hooks/useBackgroundCycle.ts +++ b/src/hooks/useBackgroundCycle.ts @@ -1,66 +1,25 @@ 'use client'; import { useUserPreferences } from '@/contexts/UserPreferencesContext'; - -// Liste des backgrounds prédéfinis pour le cycle -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' -]; +import { useToast } from '@/components/ui/Toast'; +import { BACKGROUND_NAMES, TOAST_ICONS, getNextBackground } from '@/lib/ui-config'; export function useBackgroundCycle() { const { preferences, updateViewPreferences } = useUserPreferences(); + const { showToast } = useToast(); const cycleBackground = () => { const currentBackground = preferences?.viewPreferences?.backgroundImage; const customImages = preferences?.viewPreferences?.customImages || []; - // Construire la liste complète des backgrounds (prédéfinis + personnalisés) - 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 nextBackground = getNextBackground(currentBackground || 'none', customImages); 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 }); + + // Afficher le toast avec le nom du background + const backgroundName = BACKGROUND_NAMES[nextBackground] || 'Image personnalisée'; + showToast(`Background: ${backgroundName}`, 2000, TOAST_ICONS.background); }; return { diff --git a/src/lib/theme-config.ts b/src/lib/theme-config.ts index 1b5892c..569f52b 100644 --- a/src/lib/theme-config.ts +++ b/src/lib/theme-config.ts @@ -40,7 +40,7 @@ export const THEME_CONFIG = { 'solarized' ] 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: { light: { name: 'Light', description: 'Thème clair par défaut', icon: '☀️' }, dark: { name: 'Dark', description: 'Thème sombre classique', icon: '🌙' }, diff --git a/src/lib/ui-config.ts b/src/lib/ui-config.ts new file mode 100644 index 0000000..5db52fc --- /dev/null +++ b/src/lib/ui-config.ts @@ -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 = { + 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 = { + '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 = { + '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;