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'