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.
This commit is contained in:
Julien Froidefond
2025-09-28 20:47:26 +02:00
parent 7acb2d7e4e
commit 9ef23dbddc
8 changed files with 469 additions and 17 deletions

View File

@@ -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;
}> {

View File

@@ -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);

View File

@@ -34,7 +34,10 @@ export default async function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider initialTheme={initialPreferences.viewPreferences.theme}>
<ThemeProvider
initialTheme={initialPreferences.viewPreferences.theme}
userPreferredTheme={initialPreferences.viewPreferences.theme === 'light' ? 'dark' : initialPreferences.viewPreferences.theme}
>
<JiraConfigProvider config={initialPreferences.jiraConfig}>
<UserPreferencesProvider initialPreferences={initialPreferences}>
{children}

View File

@@ -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 (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-mono font-semibold text-[var(--foreground)]">Thème de l&apos;interface</h3>
<p className="text-sm text-[var(--muted-foreground)] mt-1">
Choisissez l&apos;apparence de TowerControl
</p>
</div>
<div className="text-sm text-[var(--muted-foreground)]">
Actuel: <span className="font-medium text-[var(--primary)] capitalize">{theme}</span>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{themes.map((themeOption) => (
<button
key={themeOption.id}
onClick={() => setTheme(themeOption.id)}
className={`
p-4 rounded-lg border text-left transition-all duration-200 group
${theme === themeOption.id
? 'border-[var(--primary)] bg-[color-mix(in_srgb,var(--primary)_15%,transparent)] shadow-lg shadow-[var(--primary)]/20'
: 'border-[var(--border)] hover:border-[var(--primary)] hover:bg-[color-mix(in_srgb,var(--primary)_8%,transparent)] hover:shadow-md'
}
`}
>
<div className="flex items-start gap-3">
{/* Aperçu du thème */}
<div className="flex-shrink-0">
<div
className="w-16 h-12 rounded-lg border-2 overflow-hidden"
style={{
borderColor: theme === themeOption.id ? 'var(--primary)' : 'var(--border)',
backgroundColor: themeOption.id === 'light' ? '#f1f5f9' :
themeOption.id === 'dark' ? '#1e293b' :
themeOption.id === 'dracula' ? '#282a36' :
themeOption.id === 'monokai' ? '#272822' :
themeOption.id === 'nord' ? '#2e3440' :
themeOption.id === 'gruvbox' ? '#282828' :
themeOption.id === 'tokyo_night' ? '#1a1b26' :
themeOption.id === 'catppuccin' ? '#1e1e2e' :
themeOption.id === 'rose_pine' ? '#191724' :
themeOption.id === 'one_dark' ? '#282c34' :
themeOption.id === 'material' ? '#121212' :
'#002b36'
}}
>
{/* Barre de titre */}
<div
className="h-3 w-full"
style={{
backgroundColor: themeOption.id === 'light' ? '#ffffff' :
themeOption.id === 'dark' ? '#334155' :
themeOption.id === 'dracula' ? '#44475a' :
themeOption.id === 'monokai' ? '#3e3d32' :
themeOption.id === 'nord' ? '#3b4252' :
themeOption.id === 'gruvbox' ? '#3c3836' :
themeOption.id === 'tokyo_night' ? '#24283b' :
themeOption.id === 'catppuccin' ? '#313244' :
themeOption.id === 'rose_pine' ? '#26233a' :
themeOption.id === 'one_dark' ? '#3e4451' :
themeOption.id === 'material' ? '#1e1e1e' :
'#073642'
}}
/>
{/* Contenu avec couleurs du thème */}
<div className="p-1 h-9 flex flex-col gap-0.5">
{/* Ligne de texte */}
<div
className="h-1 rounded-sm"
style={{
backgroundColor: themeOption.id === 'light' ? '#0f172a' :
themeOption.id === 'dark' ? '#f1f5f9' :
themeOption.id === 'dracula' ? '#f8f8f2' :
themeOption.id === 'monokai' ? '#f8f8f2' :
themeOption.id === 'nord' ? '#d8dee9' :
themeOption.id === 'gruvbox' ? '#ebdbb2' :
themeOption.id === 'tokyo_night' ? '#a9b1d6' :
themeOption.id === 'catppuccin' ? '#cdd6f4' :
themeOption.id === 'rose_pine' ? '#e0def4' :
themeOption.id === 'one_dark' ? '#abb2bf' :
themeOption.id === 'material' ? '#ffffff' :
'#93a1a1'
}}
/>
{/* Couleurs d'accent */}
<div className="flex gap-0.5">
<div
className="h-1 flex-1 rounded-sm"
style={{
backgroundColor: themeOption.id === 'light' ? '#0891b2' :
themeOption.id === 'dark' ? '#06b6d4' :
themeOption.id === 'dracula' ? '#ff79c6' :
themeOption.id === 'monokai' ? '#f92672' :
themeOption.id === 'nord' ? '#88c0d0' :
themeOption.id === 'gruvbox' ? '#fe8019' :
themeOption.id === 'tokyo_night' ? '#7aa2f7' :
themeOption.id === 'catppuccin' ? '#cba6f7' :
themeOption.id === 'rose_pine' ? '#c4a7e7' :
themeOption.id === 'one_dark' ? '#61afef' :
themeOption.id === 'material' ? '#bb86fc' :
'#268bd2'
}}
/>
<div
className="h-1 flex-1 rounded-sm"
style={{
backgroundColor: themeOption.id === 'light' ? '#d97706' :
themeOption.id === 'dark' ? '#f59e0b' :
themeOption.id === 'dracula' ? '#ffb86c' :
themeOption.id === 'monokai' ? '#fd971f' :
themeOption.id === 'nord' ? '#d08770' :
themeOption.id === 'gruvbox' ? '#fabd2f' :
themeOption.id === 'tokyo_night' ? '#ff9e64' :
themeOption.id === 'catppuccin' ? '#fab387' :
themeOption.id === 'rose_pine' ? '#f6c177' :
themeOption.id === 'one_dark' ? '#e06c75' :
themeOption.id === 'material' ? '#ffab40' :
'#b58900'
}}
/>
<div
className="h-1 flex-1 rounded-sm"
style={{
backgroundColor: themeOption.id === 'light' ? '#059669' :
themeOption.id === 'dark' ? '#10b981' :
themeOption.id === 'dracula' ? '#50fa7b' :
themeOption.id === 'monokai' ? '#a6e22e' :
themeOption.id === 'nord' ? '#a3be8c' :
themeOption.id === 'gruvbox' ? '#b8bb26' :
themeOption.id === 'tokyo_night' ? '#9ece6a' :
themeOption.id === 'catppuccin' ? '#a6e3a1' :
themeOption.id === 'rose_pine' ? '#9ccfd8' :
themeOption.id === 'one_dark' ? '#98c379' :
themeOption.id === 'material' ? '#4caf50' :
'#859900'
}}
/>
</div>
</div>
</div>
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-[var(--foreground)] mb-1">
{themeOption.name}
</div>
<div className="text-xs text-[var(--muted-foreground)] leading-relaxed">
{themeOption.description}
</div>
{theme === themeOption.id && (
<div className="mt-2 text-xs text-[var(--primary)] font-medium">
Sélectionné
</div>
)}
</div>
</div>
</button>
))}
</div>
</div>
);
}

View File

@@ -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
</p>
</div>
<div className="space-y-6">
<div className="space-y-8">
{/* Sélection de thème */}
<div className="bg-[var(--card)]/30 border border-[var(--border)]/50 rounded-lg p-6 backdrop-blur-sm">
<ThemeSelector />
</div>
{/* Gestion des tags */}
<TagsManagement
tags={tags}

View File

@@ -13,7 +13,7 @@ interface HeaderProps {
}
export function Header({ title = "TowerControl", subtitle = "Task Management", syncing = false }: HeaderProps) {
const { theme, toggleTheme } = useTheme();
const { theme, toggleTheme, userPreferredTheme } = useTheme();
const { isConfigured: isJiraConfigured, config: jiraConfig } = useJiraConfig();
const pathname = usePathname();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
@@ -87,13 +87,15 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
<button
onClick={toggleTheme}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
title={theme === 'light' ? `Switch to ${userPreferredTheme} theme` : 'Switch to light theme'}
>
{theme === 'dark' ? (
{theme === 'light' ? (
// Soleil pour le thème clair
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
) : (
// Lune pour tous les thèmes sombres
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>

View File

@@ -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<ThemeContextType | undefined>(undefined);
@@ -16,10 +16,12 @@ const ThemeContext = createContext<ThemeContextType | undefined>(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<Theme>(initialTheme);
const [userPreferredTheme, setUserPreferredTheme] = useState<Theme>(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 (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme, userPreferredTheme }}>
<div className={mounted ? theme : initialTheme}>
{children}
</div>

View File

@@ -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'