feat(toolrate): light/dark/system theme toggle with FOUC guard
Build & Push Docker Image / build (push) Successful in 2m30s

- use-theme hook: localStorage (toolrate-theme, default system), matchMedia
  change listener, shared singleton listener, sets .dark + color-scheme
- ThemeToggle: lightbulb quick toggle (light/dark) + dropdown (light/dark/system)
- Sidebar footer + mobile header + standalone login page
- inline script in index.html to apply theme pre-render (no FOUC)
- recharts axis ticks use hsl(var(--foreground)) for dark-mode readability
This commit is contained in:
opencode
2026-08-01 19:47:39 +02:00
parent db397a14bc
commit d244a537ea
7 changed files with 199 additions and 18 deletions
+20 -13
View File
@@ -3,6 +3,7 @@ import { LayoutDashboard, Wrench, PlusCircle, BarChart3, LogIn, LogOut, User, Sh
import { useAuth } from "@/hooks/use-auth";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { ThemeToggle } from "@/components/theme-toggle";
export function Layout({ children }: { children: React.ReactNode }) {
const [location] = useLocation();
@@ -43,7 +44,10 @@ export function Layout({ children }: { children: React.ReactNode }) {
})}
</nav>
<div className="p-4 border-t">
<div className="p-4 border-t space-y-3">
<div className="flex justify-end">
<ThemeToggle />
</div>
{isLoading ? (
<div className="flex items-center gap-3 px-3 py-2">
<Skeleton className="w-8 h-8 rounded-full" />
@@ -97,18 +101,21 @@ export function Layout({ children }: { children: React.ReactNode }) {
<Wrench className="w-5 h-5" />
<span>ToolRate</span>
</div>
{!isLoading && (
isAuthenticated ? (
<Button variant="ghost" size="sm" onClick={logout} data-testid="button-logout-mobile">
<LogOut className="w-4 h-4" />
</Button>
) : (
<Button variant="outline" size="sm" onClick={() => login(location)} data-testid="button-login-mobile">
<LogIn className="w-4 h-4 mr-1" />
Sign in
</Button>
)
)}
<div className="flex items-center gap-1">
<ThemeToggle />
{!isLoading && (
isAuthenticated ? (
<Button variant="ghost" size="sm" onClick={logout} data-testid="button-logout-mobile">
<LogOut className="w-4 h-4" />
</Button>
) : (
<Button variant="outline" size="sm" onClick={() => login(location)} data-testid="button-login-mobile">
<LogIn className="w-4 h-4 mr-1" />
Sign in
</Button>
)
)}
</div>
</header>
<div className="flex-1 p-6 md:p-8 overflow-auto">
{children}
@@ -0,0 +1,67 @@
import { ChevronDown, Lightbulb, LightbulbOff, Monitor, Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
} from "@/components/ui/dropdown-menu";
import { useTheme, type Theme } from "@/hooks/use-theme";
export function ThemeToggle() {
const { theme, setTheme, resolvedTheme } = useTheme();
const isDark = resolvedTheme === "dark";
function quickToggle() {
setTheme(isDark ? "light" : "dark");
}
return (
<div className="flex items-center gap-0.5" data-testid="theme-toggle">
<Button
variant="ghost"
size="icon"
className="h-9 w-9"
onClick={quickToggle}
title={isDark ? "Switch to light theme" : "Switch to dark theme"}
aria-label={isDark ? "Switch to light theme" : "Switch to dark theme"}
data-testid="button-theme-quick-toggle"
>
{isDark ? <Lightbulb className="w-4 h-4" /> : <LightbulbOff className="w-4 h-4" />}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-9 w-9"
aria-label="Choose theme"
title="Choose theme"
data-testid="button-theme-menu"
>
<ChevronDown className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={6}>
<DropdownMenuLabel>Theme</DropdownMenuLabel>
<DropdownMenuRadioGroup value={theme} onValueChange={(v) => setTheme(v as Theme)}>
<DropdownMenuRadioItem value="light">
<Sun className="w-4 h-4 mr-2" />
Light
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark">
<Moon className="w-4 h-4 mr-2" />
Dark
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system">
<Monitor className="w-4 h-4 mr-2" />
System
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}