diff --git a/devussy-web/src/app/globals.css b/devussy-web/src/app/globals.css index 5cdef37..145d113 100644 --- a/devussy-web/src/app/globals.css +++ b/devussy-web/src/app/globals.css @@ -115,6 +115,144 @@ --sidebar-ring: oklch(0.65 0.25 265); } +.forest { + --radius: 0.5rem; + --background: oklch(0.98 0.01 120); + --foreground: oklch(0.15 0.02 120); + --card: oklch(1 0 0); + --card-foreground: oklch(0.15 0.02 120); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.15 0.02 120); + --primary: oklch(0.35 0.15 145); + --primary-foreground: oklch(0.98 0 0); + --secondary: oklch(0.96 0.01 120); + --secondary-foreground: oklch(0.35 0.15 145); + --muted: oklch(0.96 0.01 120); + --muted-foreground: oklch(0.55 0.03 120); + --accent: oklch(0.96 0.01 120); + --accent-foreground: oklch(0.35 0.15 145); + --destructive: oklch(0.6 0.25 25); + --border: oklch(0.92 0.01 120); + --input: oklch(0.92 0.01 120); + --ring: oklch(0.35 0.15 145); + --chart-1: oklch(0.35 0.15 145); + --chart-2: oklch(0.45 0.2 170); + --chart-3: oklch(0.25 0.1 120); + --chart-4: oklch(0.55 0.15 190); + --chart-5: oklch(0.65 0.2 210); + --sidebar: oklch(0.98 0.01 120); + --sidebar-foreground: oklch(0.15 0.02 120); + --sidebar-primary: oklch(0.35 0.15 145); + --sidebar-primary-foreground: oklch(0.98 0 0); + --sidebar-accent: oklch(0.96 0.01 120); + --sidebar-accent-foreground: oklch(0.35 0.15 145); + --sidebar-border: oklch(0.92 0.01 120); + --sidebar-ring: oklch(0.35 0.15 145); +} + +.dark.forest { + --background: oklch(0.2 0.05 140); + --foreground: oklch(0.98 0.02 140); + --card: oklch(0.15 0.06 140); + --card-foreground: oklch(0.98 0.02 140); + --popover: oklch(0.15 0.06 140); + --popover-foreground: oklch(0.98 0.02 140); + --primary: oklch(0.65 0.25 145); + --primary-foreground: oklch(0.10 0.05 140); + --secondary: oklch(0.25 0.10 140); + --secondary-foreground: oklch(0.98 0.02 140); + --muted: oklch(0.25 0.10 140); + --muted-foreground: oklch(0.70 0.05 140); + --accent: oklch(0.25 0.10 140); + --accent-foreground: oklch(0.98 0.02 140); + --destructive: oklch(0.6 0.25 25); + --border: oklch(0.30 0.10 140); + --input: oklch(0.30 0.10 140); + --ring: oklch(0.65 0.25 145); + --chart-1: oklch(0.65 0.25 145); + --chart-2: oklch(0.75 0.20 170); + --chart-3: oklch(0.55 0.25 120); + --chart-4: oklch(0.85 0.15 190); + --chart-5: oklch(0.90 0.10 210); + --sidebar: oklch(0.15 0.06 140); + --sidebar-foreground: oklch(0.98 0.02 140); + --sidebar-primary: oklch(0.65 0.25 145); + --sidebar-primary-foreground: oklch(0.10 0.05 140); + --sidebar-accent: oklch(0.25 0.10 140); + --sidebar-accent-foreground: oklch(0.98 0.02 140); + --sidebar-border: oklch(0.30 0.10 140); + --sidebar-ring: oklch(0.65 0.25 145); +} + +.ocean { + --radius: 0.5rem; + --background: oklch(0.98 0.01 200); + --foreground: oklch(0.15 0.02 200); + --card: oklch(1 0 0); + --card-foreground: oklch(0.15 0.02 200); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.15 0.02 200); + --primary: oklch(0.35 0.15 225); + --primary-foreground: oklch(0.98 0 0); + --secondary: oklch(0.96 0.01 200); + --secondary-foreground: oklch(0.35 0.15 225); + --muted: oklch(0.96 0.01 200); + --muted-foreground: oklch(0.55 0.03 200); + --accent: oklch(0.96 0.01 200); + --accent-foreground: oklch(0.35 0.15 225); + --destructive: oklch(0.6 0.25 25); + --border: oklch(0.92 0.01 200); + --input: oklch(0.92 0.01 200); + --ring: oklch(0.35 0.15 225); + --chart-1: oklch(0.35 0.15 225); + --chart-2: oklch(0.45 0.2 250); + --chart-3: oklch(0.25 0.1 200); + --chart-4: oklch(0.55 0.15 270); + --chart-5: oklch(0.65 0.2 290); + --sidebar: oklch(0.98 0.01 200); + --sidebar-foreground: oklch(0.15 0.02 200); + --sidebar-primary: oklch(0.35 0.15 225); + --sidebar-primary-foreground: oklch(0.98 0 0); + --sidebar-accent: oklch(0.96 0.01 200); + --sidebar-accent-foreground: oklch(0.35 0.15 225); + --sidebar-border: oklch(0.92 0.01 200); + --sidebar-ring: oklch(0.35 0.15 225); +} + +.dark.ocean { + --background: oklch(0.2 0.05 220); + --foreground: oklch(0.98 0.02 220); + --card: oklch(0.15 0.06 220); + --card-foreground: oklch(0.98 0.02 220); + --popover: oklch(0.15 0.06 220); + --popover-foreground: oklch(0.98 0.02 220); + --primary: oklch(0.65 0.25 225); + --primary-foreground: oklch(0.10 0.05 220); + --secondary: oklch(0.25 0.10 220); + --secondary-foreground: oklch(0.98 0.02 220); + --muted: oklch(0.25 0.10 220); + --muted-foreground: oklch(0.70 0.05 220); + --accent: oklch(0.25 0.10 220); + --accent-foreground: oklch(0.98 0.02 220); + --destructive: oklch(0.6 0.25 25); + --border: oklch(0.30 0.10 220); + --input: oklch(0.30 0.10 220); + --ring: oklch(0.65 0.25 225); + --chart-1: oklch(0.65 0.25 225); + --chart-2: oklch(0.75 0.20 250); + --chart-3: oklch(0.55 0.25 200); + --chart-4: oklch(0.85 0.15 270); + --chart-5: oklch(0.90 0.10 290); + --sidebar: oklch(0.15 0.06 220); + --sidebar-foreground: oklch(0.98 0.02 220); + --sidebar-primary: oklch(0.65 0.25 225); + --sidebar-primary-foreground: oklch(0.10 0.05 220); + --sidebar-accent: oklch(0.25 0.10 220); + --sidebar-accent-foreground: oklch(0.98 0.02 220); + --sidebar-border: oklch(0.30 0.10 220); + --sidebar-ring: oklch(0.65 0.25 225); +} + @layer base { * { @apply border-border; diff --git a/devussy-web/src/app/layout.tsx b/devussy-web/src/app/layout.tsx index c4565d6..50efafc 100644 --- a/devussy-web/src/app/layout.tsx +++ b/devussy-web/src/app/layout.tsx @@ -2,6 +2,8 @@ import type { Metadata } from "next"; import { JetBrains_Mono } from "next/font/google"; import "./globals.css"; import { AppLayout } from "@/components/layout/AppLayout"; +import { ThemeProvider } from "@/components/ThemeProvider"; +import { ThemeScript } from "@/components/ThemeScript"; const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"] }); @@ -16,12 +18,17 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + + + + -
- - {children} - + +
+ + {children} + + ); diff --git a/devussy-web/src/components/ThemeProvider.tsx b/devussy-web/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..2290a88 --- /dev/null +++ b/devussy-web/src/components/ThemeProvider.tsx @@ -0,0 +1,74 @@ +"use client"; + +import React, { createContext, useContext, useState, useEffect } from 'react'; + +const themes = ["light", "dark", "forest", "dark forest", "ocean", "dark ocean"] as const; +type Theme = typeof themes[number]; + +interface ThemeProviderProps { + children: React.ReactNode; + defaultTheme?: Theme; + storageKey?: string; +} + +interface ThemeProviderState { + theme: Theme; + setTheme: (theme: Theme) => void; + toggleTheme: () => void; +} + +const ThemeProviderContext = createContext(undefined); + +export function ThemeProvider({ + children, + defaultTheme = "dark", + storageKey = "devussy-web-theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState(() => { + try { + return (localStorage.getItem(storageKey) as Theme) || defaultTheme; + } catch (e) { + console.error("Failed to access localStorage:", e); + return defaultTheme; + } + }); + + useEffect(() => { + const root = window.document.documentElement; + root.className = theme.split(' ').join(' '); + }, [theme]); + + const toggleTheme = () => { + const currentIndex = themes.indexOf(theme); + const nextIndex = (currentIndex + 1) % themes.length; + setTheme(themes[nextIndex]); + }; + + const value = { + theme, + setTheme: (newTheme: Theme) => { + try { + localStorage.setItem(storageKey, newTheme); + } catch (e) { + console.error("Failed to set theme in localStorage:", e); + } + setTheme(newTheme); + }, + toggleTheme, + }; + + return ( + + {children} + + ); +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext); + if (context === undefined) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +}; diff --git a/devussy-web/src/components/ThemeScript.tsx b/devussy-web/src/components/ThemeScript.tsx new file mode 100644 index 0000000..5d4a1da --- /dev/null +++ b/devussy-web/src/components/ThemeScript.tsx @@ -0,0 +1,18 @@ +"use client"; + +import React from 'react'; + +const script = ` +(function() { + try { + const theme = localStorage.getItem('devussy-web-theme') || 'dark'; + document.documentElement.className = theme.split(' ').join(' '); + } catch (e) { + console.error('Failed to apply theme from localStorage', e); + } +})(); +`; + +export function ThemeScript() { + return