diff --git a/index.html b/index.html index 802c11ff0..bec75a1bd 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,21 @@ Harper Fabric + + diff --git a/src/App.tsx b/src/App.tsx index dcac1c49d..8f2c204da 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { AppRouted } from '@/AppRouted'; import { Toaster } from '@/components/ui/sonner'; +import { ThemeProvider } from '@/hooks/useTheme'; import { useDatadog } from '@/integrations/datadog/datadog'; import { useGTM } from '@/integrations/google/gtm'; import { useReo } from '@/integrations/reo/reo'; @@ -12,12 +13,12 @@ export function App() { useDatadog(); useGTM(); return ( - <> + {!import.meta.env.VITE_DISABLE_DEVTOOLS && } - + ); } diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index af45a2b9e..313b5a986 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,5 +1,6 @@ import { DiscordLogo } from '@/components/DiscordLogo'; import { MainLogo } from '@/components/MainLogo'; +import { ThemeToggle } from '@/components/ThemeToggle'; import { NavigationMenu } from '@/components/ui/navigation/NavigationMenu'; import { NavigationMenuItem } from '@/components/ui/navigation/NavigationMenuItem'; import { NavigationMenuLink } from '@/components/ui/navigation/NavigationMenuLink'; @@ -177,6 +178,9 @@ function AnonymousNav() { + + + @@ -216,6 +220,9 @@ function DesktopNav({ menuItems }: { menuItems: Array }) { ) : )} + + + @@ -278,6 +285,9 @@ function MobileNav({ menuItems }: { menuItems: Array }) { isMenuOpen ? 'block' : 'hidden' } md:hidden z-50 space-y-1 pb-3 bg-card border-b border-border dark:bg-black-dark dark:border-none absolute left-0 top-full w-full rounded-b-md`} > +
+ +
{menuItems.map(menuItem => isMenuGroup(menuItem) ? !!menuItem.items.length && ( diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx new file mode 100644 index 000000000..4b20e1fce --- /dev/null +++ b/src/components/ThemeToggle.tsx @@ -0,0 +1,31 @@ +import { type Theme, useTheme } from '@/hooks/useTheme'; +import { MonitorIcon, MoonIcon, SunIcon } from 'lucide-react'; + +const OPTIONS: Array<{ value: Theme; icon: React.ReactNode; label: string }> = [ + { value: 'light', icon: , label: 'Light' }, + { value: 'system', icon: , label: 'System' }, + { value: 'dark', icon: , label: 'Dark' }, +]; + +export function ThemeToggle() { + const [theme, setTheme] = useTheme(); + return ( +
+ {OPTIONS.map(({ value, icon, label }) => ( + + ))} +
+ ); +} diff --git a/src/features/auth/AuthLayout.tsx b/src/features/auth/AuthLayout.tsx index efd2987d8..740c1628f 100644 --- a/src/features/auth/AuthLayout.tsx +++ b/src/features/auth/AuthLayout.tsx @@ -4,7 +4,7 @@ import { Outlet } from '@tanstack/react-router'; export function AuthLayout() { return ( <> -
+
diff --git a/src/features/layouts/Dashboard.tsx b/src/features/layouts/Dashboard.tsx index a67e6a11b..be279c5cd 100644 --- a/src/features/layouts/Dashboard.tsx +++ b/src/features/layouts/Dashboard.tsx @@ -22,7 +22,7 @@ export function Dashboard() { return ( <> -
+
diff --git a/src/hooks/useTheme.tsx b/src/hooks/useTheme.tsx new file mode 100644 index 000000000..fe97ca5de --- /dev/null +++ b/src/hooks/useTheme.tsx @@ -0,0 +1,24 @@ +import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useSystemTheme } from '@/hooks/useSystemTheme'; +import { LocalStorageKeys } from '@/lib/storage/localStorageKeys'; +import { createContext, ReactNode, useContext, useEffect } from 'react'; + +export type Theme = 'system' | 'light' | 'dark'; + +const ThemeContext = createContext<[Theme, (t: Theme) => void]>(['system', () => {}]); + +export function ThemeProvider({ children }: { children: ReactNode }) { + const [theme, setTheme] = useLocalStorage(LocalStorageKeys.Theme, 'system'); + const systemTheme = useSystemTheme(); + + useEffect(() => { + const isDark = theme === 'dark' || (theme === 'system' && systemTheme === 'dark'); + document.documentElement.classList.toggle('dark', isDark); + }, [theme, systemTheme]); + + return {children}; +} + +export function useTheme() { + return useContext(ThemeContext); +} diff --git a/src/index.css b/src/index.css index 70c2e6199..8cd09e102 100644 --- a/src/index.css +++ b/src/index.css @@ -12,7 +12,7 @@ @plugin "tailwindcss-animate"; -@custom-variant dark (@media (prefers-color-scheme: dark)); +@custom-variant dark (&:where(.dark, .dark *)); :root { --white: hsl(0, 0%, 99.61%); /* #fefefe */ @@ -54,7 +54,7 @@ --purple-gradient: 90deg, var(--purple-500) 100%, var(--purple-500) 100%; --purple-dark-to-light-gradient: 45deg, var(--purple-200) 0%, var(--purple-100) 100%; - --background: hsl(0 0% 97%); /* Light grey page background */ + --background: hsl(250 30% 97%); /* Very light lavender page background */ --foreground: hsl(0 0% 9%); /* Near-black text */ --card: hsl(0 0% 100%); /* White cards */ --card-foreground: hsl(0 0% 9%); @@ -100,9 +100,8 @@ } -@media (prefers-color-scheme: dark) { - :root { - --background: var(--color-black-dark); /* Dark Black: #111111 */ +.dark { + --background: hsl(250 20% 8%); /* Deep purple-tinted dark background */ --foreground: hsl(0 0% 98%); --card: var(--color-grey-700); --card-foreground: var(--color-white); @@ -142,7 +141,6 @@ --chart-tooltip-fg: var(--popover-foreground); --shadow-deep: 0 2px 8px 0 rgba(0,0,0,0.4), 0 1px 2px 0 rgba(0,0,0,0.3); - } } @theme inline { @@ -316,8 +314,6 @@ background-size: cover; } -@media (prefers-color-scheme: light) { - .fabricSignupTextContainer { - filter: brightness(1.3) saturate(0.75); - } +html:not(.dark) .fabricSignupTextContainer { + filter: brightness(1.3) saturate(0.75); } diff --git a/src/lib/storage/localStorageKeys.ts b/src/lib/storage/localStorageKeys.ts index 0c1827ff8..91f02b994 100644 --- a/src/lib/storage/localStorageKeys.ts +++ b/src/lib/storage/localStorageKeys.ts @@ -3,4 +3,5 @@ export const enum LocalStorageKeys { 'ApplicationChatWidth' = 'ApplicationChatWidth', 'ChatAlwaysApprovedTools' = 'ChatAlwaysApprovedTools', 'SavedClusterState' = 'SavedClusterState', + 'Theme' = 'Theme', }