Spaces:
Running
Running
Kumari Vaishnavi commited on
Commit ·
38bb8dc
1
Parent(s): d85fcca
feat(ui): add dark/light theme toggle with next-themes
Browse files
frontend/package-lock.json
CHANGED
|
@@ -13,6 +13,7 @@
|
|
| 13 |
"clsx": "^2.1.1",
|
| 14 |
"lucide-react": "^1.8.0",
|
| 15 |
"next": "16.2.4",
|
|
|
|
| 16 |
"pdfjs-dist": "^5.6.205",
|
| 17 |
"react": "19.2.4",
|
| 18 |
"react-dom": "19.2.4",
|
|
@@ -8606,6 +8607,16 @@
|
|
| 8606 |
}
|
| 8607 |
}
|
| 8608 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8609 |
"node_modules/next/node_modules/postcss": {
|
| 8610 |
"version": "8.4.31",
|
| 8611 |
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
|
|
|
| 13 |
"clsx": "^2.1.1",
|
| 14 |
"lucide-react": "^1.8.0",
|
| 15 |
"next": "16.2.4",
|
| 16 |
+
"next-themes": "^0.4.6",
|
| 17 |
"pdfjs-dist": "^5.6.205",
|
| 18 |
"react": "19.2.4",
|
| 19 |
"react-dom": "19.2.4",
|
|
|
|
| 8607 |
}
|
| 8608 |
}
|
| 8609 |
},
|
| 8610 |
+
"node_modules/next-themes": {
|
| 8611 |
+
"version": "0.4.6",
|
| 8612 |
+
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
|
| 8613 |
+
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
|
| 8614 |
+
"license": "MIT",
|
| 8615 |
+
"peerDependencies": {
|
| 8616 |
+
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
| 8617 |
+
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
| 8618 |
+
}
|
| 8619 |
+
},
|
| 8620 |
"node_modules/next/node_modules/postcss": {
|
| 8621 |
"version": "8.4.31",
|
| 8622 |
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
frontend/package.json
CHANGED
|
@@ -14,6 +14,7 @@
|
|
| 14 |
"clsx": "^2.1.1",
|
| 15 |
"lucide-react": "^1.8.0",
|
| 16 |
"next": "16.2.4",
|
|
|
|
| 17 |
"pdfjs-dist": "^5.6.205",
|
| 18 |
"react": "19.2.4",
|
| 19 |
"react-dom": "19.2.4",
|
|
|
|
| 14 |
"clsx": "^2.1.1",
|
| 15 |
"lucide-react": "^1.8.0",
|
| 16 |
"next": "16.2.4",
|
| 17 |
+
"next-themes": "^0.4.6",
|
| 18 |
"pdfjs-dist": "^5.6.205",
|
| 19 |
"react": "19.2.4",
|
| 20 |
"react-dom": "19.2.4",
|
frontend/src/app/globals.css
CHANGED
|
@@ -83,6 +83,35 @@
|
|
| 83 |
--sidebar-ring: oklch(0.65 0.2 265);
|
| 84 |
}
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
.light {
|
| 87 |
--background: oklch(0.985 0 0);
|
| 88 |
--foreground: oklch(0.145 0 0);
|
|
|
|
| 83 |
--sidebar-ring: oklch(0.65 0.2 265);
|
| 84 |
}
|
| 85 |
|
| 86 |
+
.dark {
|
| 87 |
+
--background: oklch(0.145 0 0);
|
| 88 |
+
--foreground: oklch(0.985 0 0);
|
| 89 |
+
--card: oklch(0.178 0 0);
|
| 90 |
+
--card-foreground: oklch(0.985 0 0);
|
| 91 |
+
--popover: oklch(0.178 0 0);
|
| 92 |
+
--popover-foreground: oklch(0.985 0 0);
|
| 93 |
+
--primary: oklch(0.65 0.2 265);
|
| 94 |
+
--primary-foreground: oklch(0.985 0 0);
|
| 95 |
+
--secondary: oklch(0.22 0 0);
|
| 96 |
+
--secondary-foreground: oklch(0.985 0 0);
|
| 97 |
+
--muted: oklch(0.22 0 0);
|
| 98 |
+
--muted-foreground: oklch(0.6 0 0);
|
| 99 |
+
--accent: oklch(0.55 0.18 265);
|
| 100 |
+
--accent-foreground: oklch(0.985 0 0);
|
| 101 |
+
--destructive: oklch(0.704 0.191 22.216);
|
| 102 |
+
--border: oklch(1 0 0 / 10%);
|
| 103 |
+
--input: oklch(1 0 0 / 12%);
|
| 104 |
+
--ring: oklch(0.65 0.2 265);
|
| 105 |
+
--sidebar: oklch(0.12 0 0);
|
| 106 |
+
--sidebar-foreground: oklch(0.985 0 0);
|
| 107 |
+
--sidebar-primary: oklch(0.65 0.2 265);
|
| 108 |
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
| 109 |
+
--sidebar-accent: oklch(0.22 0 0);
|
| 110 |
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
| 111 |
+
--sidebar-border: oklch(1 0 0 / 8%);
|
| 112 |
+
--sidebar-ring: oklch(0.65 0.2 265);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
.light {
|
| 116 |
--background: oklch(0.985 0 0);
|
| 117 |
--foreground: oklch(0.145 0 0);
|
frontend/src/app/layout.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { Inter } from "next/font/google";
|
|
| 3 |
import "./globals.css";
|
| 4 |
import { AuthProvider } from "@/lib/auth";
|
| 5 |
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
|
|
| 6 |
|
| 7 |
const inter = Inter({
|
| 8 |
variable: "--font-sans",
|
|
@@ -23,14 +24,21 @@ export default function RootLayout({
|
|
| 23 |
children: React.ReactNode;
|
| 24 |
}>) {
|
| 25 |
return (
|
| 26 |
-
<html lang="en" className={`${inter.variable}
|
| 27 |
<body className="min-h-full flex flex-col bg-background text-foreground">
|
| 28 |
-
<
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
</body>
|
| 34 |
</html>
|
| 35 |
);
|
| 36 |
-
}
|
|
|
|
| 3 |
import "./globals.css";
|
| 4 |
import { AuthProvider } from "@/lib/auth";
|
| 5 |
import { TooltipProvider } from "@/components/ui/tooltip";
|
| 6 |
+
import { ThemeProvider } from "@/components/layout/ThemeProvider";
|
| 7 |
|
| 8 |
const inter = Inter({
|
| 9 |
variable: "--font-sans",
|
|
|
|
| 24 |
children: React.ReactNode;
|
| 25 |
}>) {
|
| 26 |
return (
|
| 27 |
+
<html lang="en" className={`${inter.variable} h-full antialiased`} suppressHydrationWarning>
|
| 28 |
<body className="min-h-full flex flex-col bg-background text-foreground">
|
| 29 |
+
<ThemeProvider
|
| 30 |
+
attribute="class"
|
| 31 |
+
defaultTheme="dark"
|
| 32 |
+
enableSystem={false}
|
| 33 |
+
disableTransitionOnChange
|
| 34 |
+
>
|
| 35 |
+
<AuthProvider>
|
| 36 |
+
<TooltipProvider>
|
| 37 |
+
{children}
|
| 38 |
+
</TooltipProvider>
|
| 39 |
+
</AuthProvider>
|
| 40 |
+
</ThemeProvider>
|
| 41 |
</body>
|
| 42 |
</html>
|
| 43 |
);
|
| 44 |
+
}
|
frontend/src/components/layout/Header.tsx
CHANGED
|
@@ -21,7 +21,8 @@ import {
|
|
| 21 |
Moon,
|
| 22 |
Sun,
|
| 23 |
} from "lucide-react";
|
| 24 |
-
import {
|
|
|
|
| 25 |
|
| 26 |
interface HeaderProps {
|
| 27 |
sidebarOpen: boolean;
|
|
@@ -33,19 +34,13 @@ interface HeaderProps {
|
|
| 33 |
export default function Header({ sidebarOpen, onToggleSidebar, viewerOpen, onToggleViewer }: HeaderProps) {
|
| 34 |
const { user, logout } = useAuth();
|
| 35 |
const router = useRouter();
|
| 36 |
-
const
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
html.classList.add("light");
|
| 43 |
-
} else {
|
| 44 |
-
html.classList.remove("light");
|
| 45 |
-
html.classList.add("dark");
|
| 46 |
-
}
|
| 47 |
-
setIsDark(!isDark);
|
| 48 |
-
};
|
| 49 |
|
| 50 |
const handleLogout = () => {
|
| 51 |
logout();
|
|
@@ -74,9 +69,11 @@ export default function Header({ sidebarOpen, onToggleSidebar, viewerOpen, onTog
|
|
| 74 |
{viewerOpen ? <PanelRightClose className="w-4 h-4" /> : <PanelRightOpen className="w-4 h-4" />}
|
| 75 |
</Button>
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
| 80 |
|
| 81 |
<DropdownMenu>
|
| 82 |
<DropdownMenuTrigger className="flex items-center h-8 gap-2 px-2 rounded-md hover:bg-accent transition-colors cursor-pointer">
|
|
@@ -102,4 +99,4 @@ export default function Header({ sidebarOpen, onToggleSidebar, viewerOpen, onTog
|
|
| 102 |
</div>
|
| 103 |
</header>
|
| 104 |
);
|
| 105 |
-
}
|
|
|
|
| 21 |
Moon,
|
| 22 |
Sun,
|
| 23 |
} from "lucide-react";
|
| 24 |
+
import { useTheme } from "next-themes";
|
| 25 |
+
import { useEffect, useState } from "react";
|
| 26 |
|
| 27 |
interface HeaderProps {
|
| 28 |
sidebarOpen: boolean;
|
|
|
|
| 34 |
export default function Header({ sidebarOpen, onToggleSidebar, viewerOpen, onToggleViewer }: HeaderProps) {
|
| 35 |
const { user, logout } = useAuth();
|
| 36 |
const router = useRouter();
|
| 37 |
+
const { theme, setTheme } = useTheme();
|
| 38 |
+
const [mounted, setMounted] = useState(false);
|
| 39 |
|
| 40 |
+
useEffect(() => setMounted(true), []);
|
| 41 |
+
|
| 42 |
+
const isDark = theme === "dark";
|
| 43 |
+
const toggleTheme = () => setTheme(isDark ? "light" : "dark");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
const handleLogout = () => {
|
| 46 |
logout();
|
|
|
|
| 69 |
{viewerOpen ? <PanelRightClose className="w-4 h-4" /> : <PanelRightOpen className="w-4 h-4" />}
|
| 70 |
</Button>
|
| 71 |
|
| 72 |
+
{mounted && (
|
| 73 |
+
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={toggleTheme} title={isDark ? "Light mode" : "Dark mode"}>
|
| 74 |
+
{isDark ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
| 75 |
+
</Button>
|
| 76 |
+
)}
|
| 77 |
|
| 78 |
<DropdownMenu>
|
| 79 |
<DropdownMenuTrigger className="flex items-center h-8 gap-2 px-2 rounded-md hover:bg-accent transition-colors cursor-pointer">
|
|
|
|
| 99 |
</div>
|
| 100 |
</header>
|
| 101 |
);
|
| 102 |
+
}
|
frontend/src/components/layout/ThemeProvider.tsx
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
| 4 |
+
import { type ThemeProviderProps } from "next-themes";
|
| 5 |
+
|
| 6 |
+
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
| 7 |
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
| 8 |
+
}
|
frontend/src/components/layout/ThemeToggle.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useTheme } from "next-themes";
|
| 4 |
+
import { useEffect, useState } from "react";
|
| 5 |
+
import { Sun, Moon } from "lucide-react";
|
| 6 |
+
|
| 7 |
+
export function ThemeToggle() {
|
| 8 |
+
const { theme, setTheme } = useTheme();
|
| 9 |
+
const [mounted, setMounted] = useState(false);
|
| 10 |
+
|
| 11 |
+
// Avoid hydration mismatch
|
| 12 |
+
useEffect(() => setMounted(true), []);
|
| 13 |
+
if (!mounted) return null;
|
| 14 |
+
|
| 15 |
+
return (
|
| 16 |
+
<button
|
| 17 |
+
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
| 18 |
+
aria-label="Toggle theme"
|
| 19 |
+
className="rounded-md p-2 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
|
| 20 |
+
>
|
| 21 |
+
{theme === "dark" ? (
|
| 22 |
+
<Sun className="h-5 w-5 text-yellow-400" />
|
| 23 |
+
) : (
|
| 24 |
+
<Moon className="h-5 w-5 text-gray-700" />
|
| 25 |
+
)}
|
| 26 |
+
</button>
|
| 27 |
+
);
|
| 28 |
+
}
|