feat: implement 4-theme system with Zustand persistence
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import { ThemeProvider } from '@/components/providers/theme-provider';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
@@ -15,8 +16,10 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="es">
|
||||
<body className={inter.className}>{children}</body>
|
||||
<html lang="es" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
26
apps/web/components/providers/theme-provider.tsx
Normal file
26
apps/web/components/providers/theme-provider.tsx
Normal 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
6
apps/web/lib/utils.ts
Normal 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));
|
||||
}
|
||||
20
apps/web/stores/theme-store.ts
Normal file
20
apps/web/stores/theme-store.ts
Normal 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',
|
||||
}
|
||||
)
|
||||
);
|
||||
32
apps/web/themes/corporate.ts
Normal file
32
apps/web/themes/corporate.ts
Normal 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
36
apps/web/themes/dark.ts
Normal 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
16
apps/web/themes/index.ts
Normal 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
31
apps/web/themes/light.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
31
apps/web/themes/vibrant.ts
Normal file
31
apps/web/themes/vibrant.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user