feat: implement 4-theme system with Zustand persistence

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Consultoria AS
2026-01-22 01:58:24 +00:00
parent cbc48cfe26
commit 4389f50e7d
9 changed files with 203 additions and 2 deletions

View File

@@ -1,6 +1,7 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Inter } from 'next/font/google'; import { Inter } from 'next/font/google';
import './globals.css'; import './globals.css';
import { ThemeProvider } from '@/components/providers/theme-provider';
const inter = Inter({ subsets: ['latin'] }); const inter = Inter({ subsets: ['latin'] });
@@ -15,8 +16,10 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="es"> <html lang="es" suppressHydrationWarning>
<body className={inter.className}>{children}</body> <body className={inter.className}>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html> </html>
); );
} }

View File

@@ -0,0 +1,26 @@
'use client';
import { useEffect } from 'react';
import { useThemeStore } from '@/stores/theme-store';
import { themes } from '@/themes';
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const { theme } = useThemeStore();
useEffect(() => {
const selectedTheme = themes[theme];
const root = document.documentElement;
Object.entries(selectedTheme.cssVars).forEach(([key, value]) => {
root.style.setProperty(key, value);
});
if (theme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}, [theme]);
return <>{children}</>;
}

6
apps/web/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,20 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { ThemeName } from '@/themes';
interface ThemeState {
theme: ThemeName;
setTheme: (theme: ThemeName) => void;
}
export const useThemeStore = create<ThemeState>()(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{
name: 'horux-theme',
}
)
);

View File

@@ -0,0 +1,32 @@
export const corporateTheme = {
name: 'corporate' as const,
label: 'Corporate',
layout: 'multi-panel',
cssVars: {
'--background': '210 20% 96%',
'--foreground': '210 50% 10%',
'--card': '0 0% 100%',
'--card-foreground': '210 50% 10%',
'--primary': '210 100% 25%',
'--primary-foreground': '0 0% 100%',
'--secondary': '210 20% 92%',
'--secondary-foreground': '210 50% 10%',
'--muted': '210 20% 92%',
'--muted-foreground': '210 15% 45%',
'--accent': '43 96% 56%',
'--accent-foreground': '210 50% 10%',
'--destructive': '0 84.2% 60.2%',
'--destructive-foreground': '210 40% 98%',
'--success': '142.1 76.2% 36.3%',
'--success-foreground': '355.7 100% 97.3%',
'--border': '210 20% 85%',
'--input': '210 20% 85%',
'--ring': '210 100% 25%',
'--radius': '0.25rem',
},
sidebar: {
width: '200px',
collapsible: false,
},
density: 'compact',
};

36
apps/web/themes/dark.ts Normal file
View File

@@ -0,0 +1,36 @@
export const darkTheme = {
name: 'dark' as const,
label: 'Dark',
layout: 'minimal-floating',
cssVars: {
'--background': '0 0% 3.9%',
'--foreground': '0 0% 98%',
'--card': '0 0% 6%',
'--card-foreground': '0 0% 98%',
'--primary': '187.2 85.7% 53.3%',
'--primary-foreground': '0 0% 3.9%',
'--secondary': '0 0% 12%',
'--secondary-foreground': '0 0% 98%',
'--muted': '0 0% 12%',
'--muted-foreground': '0 0% 63.9%',
'--accent': '142.1 70.6% 45.3%',
'--accent-foreground': '0 0% 3.9%',
'--destructive': '0 62.8% 30.6%',
'--destructive-foreground': '0 0% 98%',
'--success': '142.1 70.6% 45.3%',
'--success-foreground': '144.9 80.4% 10%',
'--border': '0 0% 14.9%',
'--input': '0 0% 14.9%',
'--ring': '187.2 85.7% 53.3%',
'--radius': '0.75rem',
},
sidebar: {
width: '64px',
collapsible: false,
iconsOnly: true,
},
effects: {
blur: '10px',
glow: '0 0 20px rgba(34,211,238,0.3)',
},
};

16
apps/web/themes/index.ts Normal file
View File

@@ -0,0 +1,16 @@
import { lightTheme } from './light';
import { vibrantTheme } from './vibrant';
import { corporateTheme } from './corporate';
import { darkTheme } from './dark';
export const themes = {
light: lightTheme,
vibrant: vibrantTheme,
corporate: corporateTheme,
dark: darkTheme,
} as const;
export type ThemeName = keyof typeof themes;
export type Theme = (typeof themes)[ThemeName];
export { lightTheme, vibrantTheme, corporateTheme, darkTheme };

31
apps/web/themes/light.ts Normal file
View File

@@ -0,0 +1,31 @@
export const lightTheme = {
name: 'light' as const,
label: 'Light',
layout: 'sidebar-fixed',
cssVars: {
'--background': '0 0% 100%',
'--foreground': '222.2 84% 4.9%',
'--card': '0 0% 100%',
'--card-foreground': '222.2 84% 4.9%',
'--primary': '221.2 83.2% 53.3%',
'--primary-foreground': '210 40% 98%',
'--secondary': '210 40% 96.1%',
'--secondary-foreground': '222.2 47.4% 11.2%',
'--muted': '210 40% 96.1%',
'--muted-foreground': '215.4 16.3% 46.9%',
'--accent': '210 40% 96.1%',
'--accent-foreground': '222.2 47.4% 11.2%',
'--destructive': '0 84.2% 60.2%',
'--destructive-foreground': '210 40% 98%',
'--success': '142.1 76.2% 36.3%',
'--success-foreground': '355.7 100% 97.3%',
'--border': '214.3 31.8% 91.4%',
'--input': '214.3 31.8% 91.4%',
'--ring': '221.2 83.2% 53.3%',
'--radius': '0.5rem',
},
sidebar: {
width: '240px',
collapsible: false,
},
};

View File

@@ -0,0 +1,31 @@
export const vibrantTheme = {
name: 'vibrant' as const,
label: 'Vibrant',
layout: 'sidebar-collapsible',
cssVars: {
'--background': '270 50% 98%',
'--foreground': '263.4 84% 6.7%',
'--card': '0 0% 100%',
'--card-foreground': '263.4 84% 6.7%',
'--primary': '262.1 83.3% 57.8%',
'--primary-foreground': '210 40% 98%',
'--secondary': '187 85.7% 53.3%',
'--secondary-foreground': '222.2 47.4% 11.2%',
'--muted': '270 30% 94%',
'--muted-foreground': '263.4 25% 40%',
'--accent': '24.6 95% 53.1%',
'--accent-foreground': '0 0% 100%',
'--destructive': '0 84.2% 60.2%',
'--destructive-foreground': '210 40% 98%',
'--success': '142.1 76.2% 36.3%',
'--success-foreground': '355.7 100% 97.3%',
'--border': '270 30% 88%',
'--input': '270 30% 88%',
'--ring': '262.1 83.3% 57.8%',
'--radius': '1rem',
},
sidebar: {
width: '280px',
collapsible: true,
},
};