feat(toolrate): light/dark/system theme toggle with FOUC guard
Build & Push Docker Image / build (push) Successful in 2m30s
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user