Skip to content

Commit 330c7aa

Browse files
authored
feat: add support for light mode (#438)
* feat: add support for light mode * refactor: use shadcn theme toggle * fix: fix sonner
1 parent 0227af6 commit 330c7aa

12 files changed

Lines changed: 415 additions & 28 deletions

File tree

‎frontend/bun.lock‎

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎frontend/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<link rel="manifest" href="/site.webmanifest" />
1313
<title>Tinyauth</title>
1414
</head>
15-
<body class="dark">
15+
<body>
1616
<div id="root"></div>
1717
<script type="module" src="/src/main.tsx"></script>
1818
</body>

‎frontend/package.json‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
"dev": "vite",
88
"build": "tsc -b && vite build",
99
"lint": "eslint .",
10-
"preview": "vite preview"
10+
"preview": "vite preview",
11+
"tsc": "tsc -b"
1112
},
1213
"dependencies": {
1314
"@hookform/resolvers": "^5.2.2",
15+
"@radix-ui/react-dropdown-menu": "^2.1.16",
1416
"@radix-ui/react-label": "^2.1.7",
1517
"@radix-ui/react-select": "^2.2.6",
1618
"@radix-ui/react-separator": "^1.1.7",
@@ -54,4 +56,4 @@
5456
"typescript-eslint": "^8.46.1",
5557
"vite": "^7.1.10"
5658
}
57-
}
59+
}

‎frontend/src/components/language/language.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ export const LanguageSelector = () => {
1818
setLanguage(option as SupportedLanguage);
1919
i18n.changeLanguage(option as SupportedLanguage);
2020
};
21+
2122
return (
2223
<Select onValueChange={handleSelect} value={language}>
23-
<SelectTrigger className="absolute top-5 right-5">
24+
<SelectTrigger>
2425
<SelectValue placeholder="Select language" />
2526
</SelectTrigger>
2627
<SelectContent>

‎frontend/src/components/layout/layout.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { LanguageSelector } from "../language/language";
33
import { Outlet } from "react-router";
44
import { useCallback, useEffect, useState } from "react";
55
import { DomainWarning } from "../domain-warning/domain-warning";
6+
import { ThemeToggle } from "../theme-toggle/theme-toggle";
67

78
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
89
const { backgroundImage, title } = useAppContext();
@@ -20,7 +21,10 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => {
2021
backgroundPosition: "center",
2122
}}
2223
>
23-
<LanguageSelector />
24+
<div className="absolute top-5 right-5 flex flex-row gap-2">
25+
<ThemeToggle />
26+
<LanguageSelector />
27+
</div>
2428
{children}
2529
</div>
2630
);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { createContext, useContext, useEffect, useState } from "react";
2+
3+
type Theme = "dark" | "light" | "system";
4+
5+
type ThemeProviderProps = {
6+
children: React.ReactNode;
7+
defaultTheme?: Theme;
8+
storageKey?: string;
9+
};
10+
11+
type ThemeProviderState = {
12+
theme: Theme;
13+
setTheme: (theme: Theme) => void;
14+
};
15+
16+
const initialState: ThemeProviderState = {
17+
theme: "system",
18+
setTheme: () => null,
19+
};
20+
21+
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
22+
23+
export function ThemeProvider({
24+
children,
25+
defaultTheme = "system",
26+
storageKey = "vite-ui-theme",
27+
...props
28+
}: ThemeProviderProps) {
29+
const [theme, setTheme] = useState<Theme>(
30+
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
31+
);
32+
33+
useEffect(() => {
34+
const root = window.document.documentElement;
35+
36+
root.classList.remove("light", "dark");
37+
38+
if (theme === "system") {
39+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40+
.matches
41+
? "dark"
42+
: "light";
43+
44+
root.classList.add(systemTheme);
45+
return;
46+
}
47+
48+
root.classList.add(theme);
49+
}, [theme]);
50+
51+
const value = {
52+
theme,
53+
setTheme: (theme: Theme) => {
54+
localStorage.setItem(storageKey, theme);
55+
setTheme(theme);
56+
},
57+
};
58+
59+
return (
60+
<ThemeProviderContext.Provider {...props} value={value}>
61+
{children}
62+
</ThemeProviderContext.Provider>
63+
);
64+
}
65+
66+
export const useTheme = () => {
67+
const context = useContext(ThemeProviderContext);
68+
69+
if (context === undefined)
70+
throw new Error("useTheme must be used within a ThemeProvider");
71+
72+
return context;
73+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Moon, Sun } from "lucide-react";
2+
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
DropdownMenu,
6+
DropdownMenuContent,
7+
DropdownMenuItem,
8+
DropdownMenuTrigger,
9+
} from "@/components/ui/dropdown-menu";
10+
import { useTheme } from "@/components/providers/theme-provider";
11+
12+
export function ThemeToggle() {
13+
const { setTheme } = useTheme();
14+
15+
return (
16+
<DropdownMenu>
17+
<DropdownMenuTrigger asChild>
18+
<Button
19+
className="bg-card text-card-foreground hover:bg-card/90"
20+
size="icon"
21+
>
22+
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
23+
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
24+
<span className="sr-only">Toggle theme</span>
25+
</Button>
26+
</DropdownMenuTrigger>
27+
<DropdownMenuContent align="end">
28+
<DropdownMenuItem onClick={() => setTheme("light")}>
29+
Light
30+
</DropdownMenuItem>
31+
<DropdownMenuItem onClick={() => setTheme("dark")}>
32+
Dark
33+
</DropdownMenuItem>
34+
<DropdownMenuItem onClick={() => setTheme("system")}>
35+
System
36+
</DropdownMenuItem>
37+
</DropdownMenuContent>
38+
</DropdownMenu>
39+
);
40+
}

‎frontend/src/components/ui/button.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
66
import { Loader2 } from "lucide-react";
77

88
const buttonVariants = cva(
9-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:cursor-pointer",
1010
{
1111
variants: {
1212
variant: {

0 commit comments

Comments
 (0)