From 9ef23dbddcc35cc4b1d4b35437ff201fa628e159 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 28 Sep 2025 20:47:26 +0200 Subject: [PATCH] feat: enhance theme management and customization options - Added support for multiple themes (dracula, monokai, nord, gruvbox, tokyo_night, catppuccin, rose_pine, one_dark, material, solarized) in the application. - Updated `setTheme` function to accept the new `Theme` type, allowing for more flexible theme selection. - Introduced `ThemeSelector` component in GeneralSettingsPage for user-friendly theme selection. - Modified `ThemeProvider` to handle user preferred themes and improved theme toggling logic. - Updated CSS variables in `globals.css` to support new themes, enhancing visual consistency across the app. --- src/actions/preferences.ts | 6 +- src/app/globals.css | 240 ++++++++++++++++++ src/app/layout.tsx | 5 +- src/components/ThemeSelector.tsx | 188 ++++++++++++++ .../settings/GeneralSettingsPageClient.tsx | 8 +- src/components/ui/Header.tsx | 8 +- src/contexts/ThemeContext.tsx | 23 +- src/lib/types.ts | 8 +- 8 files changed, 469 insertions(+), 17 deletions(-) create mode 100644 src/components/ThemeSelector.tsx diff --git a/src/actions/preferences.ts b/src/actions/preferences.ts index ca74994..79c33ec 100644 --- a/src/actions/preferences.ts +++ b/src/actions/preferences.ts @@ -1,7 +1,7 @@ 'use server'; import { userPreferencesService } from '@/services/core/user-preferences'; -import { KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types'; +import { KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus, Theme } from '@/lib/types'; import { revalidatePath } from 'next/cache'; /** @@ -117,9 +117,9 @@ export async function toggleObjectivesCollapse(): Promise<{ } /** - * Change le thème (light/dark) + * Change le thème (light/dark/dracula/monokai/nord) */ -export async function setTheme(theme: 'light' | 'dark'): Promise<{ +export async function setTheme(theme: Theme): Promise<{ success: boolean; error?: string; }> { diff --git a/src/app/globals.css b/src/app/globals.css index 35de1c9..ab2795f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -48,6 +48,246 @@ --gray-light: #374151; /* gray-700 */ } +.dracula { + /* Dracula theme */ + --background: #282a36; /* dracula background */ + --foreground: #f8f8f2; /* dracula foreground */ + --card: #44475a; /* dracula current line */ + --card-hover: #6272a4; /* dracula comment */ + --card-column: #21222c; /* darker background */ + --border: #6272a4; /* dracula comment */ + --input: #44475a; /* dracula current line */ + --primary: #ff79c6; /* dracula pink */ + --primary-foreground: #282a36; /* dracula background */ + --muted: #6272a4; /* dracula comment */ + --muted-foreground: #50fa7b; /* dracula green */ + --accent: #ffb86c; /* dracula orange */ + --destructive: #ff5555; /* dracula red */ + --success: #50fa7b; /* dracula green */ + --purple: #bd93f9; /* dracula purple */ + --yellow: #f1fa8c; /* dracula yellow */ + --green: #50fa7b; /* dracula green */ + --blue: #8be9fd; /* dracula cyan */ + --gray: #6272a4; /* dracula comment */ + --gray-light: #44475a; /* dracula current line */ +} + +.monokai { + /* Monokai theme */ + --background: #272822; /* monokai background */ + --foreground: #f8f8f2; /* monokai foreground */ + --card: #3e3d32; /* monokai selection */ + --card-hover: #49483e; /* monokai line */ + --card-column: #1e1f1c; /* darker background */ + --border: #49483e; /* monokai line */ + --input: #3e3d32; /* monokai selection */ + --primary: #f92672; /* monokai pink */ + --primary-foreground: #272822; /* monokai background */ + --muted: #75715e; /* monokai comment */ + --muted-foreground: #a6e22e; /* monokai green */ + --accent: #fd971f; /* monokai orange */ + --destructive: #f92672; /* monokai red */ + --success: #a6e22e; /* monokai green */ + --purple: #ae81ff; /* monokai purple */ + --yellow: #e6db74; /* monokai yellow */ + --green: #a6e22e; /* monokai green */ + --blue: #66d9ef; /* monokai cyan */ + --gray: #75715e; /* monokai comment */ + --gray-light: #3e3d32; /* monokai selection */ +} + +.nord { + /* Nord theme */ + --background: #2e3440; /* nord0 */ + --foreground: #d8dee9; /* nord4 */ + --card: #3b4252; /* nord1 */ + --card-hover: #434c5e; /* nord2 */ + --card-column: #242831; /* darker nord0 */ + --border: #4c566a; /* nord3 */ + --input: #3b4252; /* nord1 */ + --primary: #88c0d0; /* nord7 */ + --primary-foreground: #2e3440; /* nord0 */ + --muted: #4c566a; /* nord3 */ + --muted-foreground: #81a1c1; /* nord9 */ + --accent: #d08770; /* nord12 */ + --destructive: #bf616a; /* nord11 */ + --success: #a3be8c; /* nord14 */ + --purple: #b48ead; /* nord13 */ + --yellow: #ebcb8b; /* nord15 */ + --green: #a3be8c; /* nord14 */ + --blue: #5e81ac; /* nord10 */ + --gray: #4c566a; /* nord3 */ + --gray-light: #3b4252; /* nord1 */ +} + +.gruvbox { + /* Gruvbox theme */ + --background: #282828; /* gruvbox bg0 */ + --foreground: #ebdbb2; /* gruvbox fg */ + --card: #3c3836; /* gruvbox bg1 */ + --card-hover: #504945; /* gruvbox bg2 */ + --card-column: #1d2021; /* gruvbox bg0_h */ + --border: #665c54; /* gruvbox bg3 */ + --input: #3c3836; /* gruvbox bg1 */ + --primary: #fe8019; /* gruvbox orange */ + --primary-foreground: #282828; /* gruvbox bg0 */ + --muted: #665c54; /* gruvbox bg3 */ + --muted-foreground: #a89984; /* gruvbox gray */ + --accent: #fabd2f; /* gruvbox yellow */ + --destructive: #fb4934; /* gruvbox red */ + --success: #b8bb26; /* gruvbox green */ + --purple: #d3869b; /* gruvbox purple */ + --yellow: #fabd2f; /* gruvbox yellow */ + --green: #b8bb26; /* gruvbox green */ + --blue: #83a598; /* gruvbox blue */ + --gray: #a89984; /* gruvbox gray */ + --gray-light: #3c3836; /* gruvbox bg1 */ +} + +.tokyo_night { + /* Tokyo Night theme */ + --background: #1a1b26; /* tokyo-night bg */ + --foreground: #a9b1d6; /* tokyo-night fg */ + --card: #24283b; /* tokyo-night bg_highlight */ + --card-hover: #2f3349; /* tokyo-night bg_visual */ + --card-column: #16161e; /* tokyo-night bg_dark */ + --border: #565f89; /* tokyo-night comment */ + --input: #24283b; /* tokyo-night bg_highlight */ + --primary: #7aa2f7; /* tokyo-night blue */ + --primary-foreground: #1a1b26; /* tokyo-night bg */ + --muted: #565f89; /* tokyo-night comment */ + --muted-foreground: #9aa5ce; /* tokyo-night fg_dark */ + --accent: #ff9e64; /* tokyo-night orange */ + --destructive: #f7768e; /* tokyo-night red */ + --success: #9ece6a; /* tokyo-night green */ + --purple: #bb9af7; /* tokyo-night purple */ + --yellow: #e0af68; /* tokyo-night yellow */ + --green: #9ece6a; /* tokyo-night green */ + --blue: #7aa2f7; /* tokyo-night blue */ + --gray: #565f89; /* tokyo-night comment */ + --gray-light: #24283b; /* tokyo-night bg_highlight */ +} + +.catppuccin { + /* Catppuccin Mocha theme */ + --background: #1e1e2e; /* catppuccin base */ + --foreground: #cdd6f4; /* catppuccin text */ + --card: #313244; /* catppuccin surface0 */ + --card-hover: #45475a; /* catppuccin surface1 */ + --card-column: #181825; /* catppuccin mantle */ + --border: #6c7086; /* catppuccin overlay0 */ + --input: #313244; /* catppuccin surface0 */ + --primary: #cba6f7; /* catppuccin mauve */ + --primary-foreground: #1e1e2e; /* catppuccin base */ + --muted: #6c7086; /* catppuccin overlay0 */ + --muted-foreground: #a6adc8; /* catppuccin subtext0 */ + --accent: #fab387; /* catppuccin peach */ + --destructive: #f38ba8; /* catppuccin red */ + --success: #a6e3a1; /* catppuccin green */ + --purple: #cba6f7; /* catppuccin mauve */ + --yellow: #f9e2af; /* catppuccin yellow */ + --green: #a6e3a1; /* catppuccin green */ + --blue: #89b4fa; /* catppuccin blue */ + --gray: #6c7086; /* catppuccin overlay0 */ + --gray-light: #313244; /* catppuccin surface0 */ +} + +.rose_pine { + /* Rose Pine theme */ + --background: #191724; /* rose-pine base */ + --foreground: #e0def4; /* rose-pine text */ + --card: #26233a; /* rose-pine surface */ + --card-hover: #312f44; /* rose-pine overlay */ + --card-column: #16141f; /* rose-pine base */ + --border: #6e6a86; /* rose-pine muted */ + --input: #26233a; /* rose-pine surface */ + --primary: #c4a7e7; /* rose-pine iris */ + --primary-foreground: #191724; /* rose-pine base */ + --muted: #6e6a86; /* rose-pine muted */ + --muted-foreground: #908caa; /* rose-pine subtle */ + --accent: #f6c177; /* rose-pine gold */ + --destructive: #eb6f92; /* rose-pine love */ + --success: #9ccfd8; /* rose-pine foam */ + --purple: #c4a7e7; /* rose-pine iris */ + --yellow: #f6c177; /* rose-pine gold */ + --green: #9ccfd8; /* rose-pine foam */ + --blue: #3e8fb0; /* rose-pine pine */ + --gray: #6e6a86; /* rose-pine muted */ + --gray-light: #26233a; /* rose-pine surface */ +} + +.one_dark { + /* One Dark theme */ + --background: #282c34; /* one-dark bg */ + --foreground: #abb2bf; /* one-dark fg */ + --card: #3e4451; /* one-dark bg1 */ + --card-hover: #4f5666; /* one-dark bg2 */ + --card-column: #21252b; /* one-dark bg0 */ + --border: #5c6370; /* one-dark bg3 */ + --input: #3e4451; /* one-dark bg1 */ + --primary: #61afef; /* one-dark blue */ + --primary-foreground: #282c34; /* one-dark bg */ + --muted: #5c6370; /* one-dark bg3 */ + --muted-foreground: #828997; /* one-dark gray */ + --accent: #e06c75; /* one-dark red */ + --destructive: #e06c75; /* one-dark red */ + --success: #98c379; /* one-dark green */ + --purple: #c678dd; /* one-dark purple */ + --yellow: #e5c07b; /* one-dark yellow */ + --green: #98c379; /* one-dark green */ + --blue: #61afef; /* one-dark blue */ + --gray: #5c6370; /* one-dark bg3 */ + --gray-light: #3e4451; /* one-dark bg1 */ +} + +.material { + /* Material Design Dark theme */ + --background: #121212; /* material bg */ + --foreground: #ffffff; /* material on-bg */ + --card: #1e1e1e; /* material surface */ + --card-hover: #2c2c2c; /* material surface-variant */ + --card-column: #0f0f0f; /* material surface-container */ + --border: #3c3c3c; /* material outline */ + --input: #1e1e1e; /* material surface */ + --primary: #bb86fc; /* material primary */ + --primary-foreground: #121212; /* material bg */ + --muted: #3c3c3c; /* material outline */ + --muted-foreground: #b3b3b3; /* material on-surface-variant */ + --accent: #ffab40; /* material secondary */ + --destructive: #cf6679; /* material error */ + --success: #4caf50; /* material success */ + --purple: #bb86fc; /* material primary */ + --yellow: #ffab40; /* material secondary */ + --green: #4caf50; /* material success */ + --blue: #2196f3; /* material info */ + --gray: #3c3c3c; /* material outline */ + --gray-light: #1e1e1e; /* material surface */ +} + +.solarized { + /* Solarized Dark theme */ + --background: #002b36; /* solarized base03 */ + --foreground: #93a1a1; /* solarized base1 */ + --card: #073642; /* solarized base02 */ + --card-hover: #0a4b5a; /* solarized base01 */ + --card-column: #001e26; /* solarized base03 darker */ + --border: #586e75; /* solarized base01 */ + --input: #073642; /* solarized base02 */ + --primary: #268bd2; /* solarized blue */ + --primary-foreground: #002b36; /* solarized base03 */ + --muted: #586e75; /* solarized base01 */ + --muted-foreground: #657b83; /* solarized base00 */ + --accent: #b58900; /* solarized yellow */ + --destructive: #dc322f; /* solarized red */ + --success: #859900; /* solarized green */ + --purple: #6c71c4; /* solarized violet */ + --yellow: #b58900; /* solarized yellow */ + --green: #859900; /* solarized green */ + --blue: #268bd2; /* solarized blue */ + --gray: #586e75; /* solarized base01 */ + --gray-light: #073642; /* solarized base02 */ +} + @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976c8c1..e31ec0a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -34,7 +34,10 @@ export default async function RootLayout({ - + {children} diff --git a/src/components/ThemeSelector.tsx b/src/components/ThemeSelector.tsx new file mode 100644 index 0000000..d90de4a --- /dev/null +++ b/src/components/ThemeSelector.tsx @@ -0,0 +1,188 @@ +'use client'; + +import { useTheme } from '@/contexts/ThemeContext'; +import { Theme } from '@/lib/types'; + +const themes: { id: Theme; name: string; description: string }[] = [ + { id: 'dark', name: 'Dark', description: 'Thème sombre par défaut' }, + { id: 'dracula', name: 'Dracula', description: 'Thème Dracula coloré' }, + { id: 'monokai', name: 'Monokai', description: 'Thème Monokai vibrant' }, + { id: 'nord', name: 'Nord', description: 'Thème Nord minimaliste' }, + { id: 'gruvbox', name: 'Gruvbox', description: 'Thème Gruvbox chaleureux' }, + { id: 'tokyo_night', name: 'Tokyo Night', description: 'Thème Tokyo Night moderne' }, + { 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' }, +]; + +export function ThemeSelector() { + const { theme, setTheme } = useTheme(); + + return ( +
+
+
+

Thème de l'interface

+

+ Choisissez l'apparence de TowerControl +

+
+
+ Actuel: {theme} +
+
+ +
+ {themes.map((themeOption) => ( + + ))} +
+
+ ); +} diff --git a/src/components/settings/GeneralSettingsPageClient.tsx b/src/components/settings/GeneralSettingsPageClient.tsx index d28c99d..ff79ff6 100644 --- a/src/components/settings/GeneralSettingsPageClient.tsx +++ b/src/components/settings/GeneralSettingsPageClient.tsx @@ -5,6 +5,7 @@ import { useTags } from '@/hooks/useTags'; import { Header } from '@/components/ui/Header'; import { Card, CardContent } from '@/components/ui/Card'; import { TagsManagement } from './tags/TagsManagement'; +import { ThemeSelector } from '@/components/ThemeSelector'; import Link from 'next/link'; interface GeneralSettingsPageClientProps { @@ -46,7 +47,12 @@ export function GeneralSettingsPageClient({ initialTags }: GeneralSettingsPageCl

-
+
+ {/* Sélection de thème */} +
+ +
+ {/* Gestion des tags */} - {theme === 'dark' ? ( + {theme === 'light' ? ( + // Soleil pour le thème clair ) : ( + // Lune pour tous les thèmes sombres diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index 8dcb9e2..e0cffc4 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -2,13 +2,13 @@ import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { updateViewPreferences } from '@/actions/preferences'; - -type Theme = 'light' | 'dark'; +import { Theme } from '@/lib/types'; interface ThemeContextType { theme: Theme; toggleTheme: () => void; setTheme: (theme: Theme) => void; + userPreferredTheme: Theme; } const ThemeContext = createContext(undefined); @@ -16,10 +16,12 @@ const ThemeContext = createContext(undefined); interface ThemeProviderProps { children: ReactNode; initialTheme?: Theme; + userPreferredTheme?: Theme; } -export function ThemeProvider({ children, initialTheme = 'dark' }: ThemeProviderProps) { +export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTheme: initialUserPreferredTheme = 'dark' }: ThemeProviderProps) { const [theme, setThemeState] = useState(initialTheme); + const [userPreferredTheme, setUserPreferredTheme] = useState(initialUserPreferredTheme); const [mounted, setMounted] = useState(false); // Hydration safe initialization @@ -30,12 +32,16 @@ export function ThemeProvider({ children, initialTheme = 'dark' }: ThemeProvider // Apply theme class to document useEffect(() => { if (mounted) { - document.documentElement.className = theme; + // Remove all existing theme classes + document.documentElement.classList.remove('light', 'dark', 'dracula', 'monokai', 'nord', 'gruvbox', 'tokyo_night', 'catppuccin', 'rose_pine', 'one_dark', 'material', 'solarized'); + // Add the current theme class + document.documentElement.classList.add(theme); } }, [theme, mounted]); const toggleTheme = async () => { - const newTheme = theme === 'dark' ? 'light' : 'dark'; + // Toggle between light and the user's chosen dark theme + const newTheme = theme === 'light' ? userPreferredTheme : 'light'; setThemeState(newTheme); // Sauvegarder en base de façon asynchrone via server action @@ -54,6 +60,11 @@ export function ThemeProvider({ children, initialTheme = 'dark' }: ThemeProvider const setTheme = async (newTheme: Theme) => { setThemeState(newTheme); + // Si ce n'est pas le thème light, c'est le thème préféré de l'utilisateur + if (newTheme !== 'light') { + setUserPreferredTheme(newTheme); + } + // Sauvegarder en base de façon asynchrone via server action try { const result = await updateViewPreferences({ @@ -68,7 +79,7 @@ export function ThemeProvider({ children, initialTheme = 'dark' }: ThemeProvider }; return ( - +
{children}
diff --git a/src/lib/types.ts b/src/lib/types.ts index 0036174..01ed514 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -13,6 +13,9 @@ export type TaskStatus = export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent'; export type TaskSource = 'reminders' | 'jira' | 'tfs' | 'manual'; +// Types de thèmes partagés +export type Theme = 'light' | 'dark' | 'dracula' | 'monokai' | 'nord' | 'gruvbox' | 'tokyo_night' | 'catppuccin' | 'rose_pine' | 'one_dark' | 'material' | 'solarized'; + // Interface centralisée pour les statistiques export interface TaskStats { total: number; @@ -91,14 +94,13 @@ export interface ViewPreferences { showObjectives: boolean; showFilters: boolean; objectivesCollapsed: boolean; - theme: 'light' | 'dark'; + theme: Theme; fontSize: 'small' | 'medium' | 'large'; [key: string]: | boolean | 'tags' | 'priority' - | 'light' - | 'dark' + | Theme | 'small' | 'medium' | 'large'